【问题标题】:Angular Unit test readonly variables insideAngular Unit测试里面的只读变量
【发布时间】:2022-01-10 04:31:33
【问题描述】:

我正在为以下类编写单元测试,并且有只读类变量。我需要覆盖该变量中的测试逻辑。

    import { Injectable } from '@angular/core';
    import { FormControl } from '@angular/forms';
    import { keyBy, flatMap, isBoolean, some, forEach } from 'lodash-es';
    import { Observable, forkJoin, ReplaySubject } from 'rxjs';
    import {
      switchMap,
      shareReplay,
      finalize,
      take,
      map,
      filter,
      withLatestFrom,
      tap,
      mapTo,
    } from 'rxjs/operators';
    import {
      AffirmationQuestion,
      AffirmationData,
      AffirmationType,
    } from 'src/app/models/affirmation';
    import { CorporateGovernanceService } from 'src/app/services/api/corporate-governance.service';
    import { environment } from 'src/environments/environment';
    
    const APP_ID = parseInt(localStorage.getItem('id'));
    
    export interface AffirmationRow extends AffirmationQuestion {
      readonly section: string;
      readonly answer: FormControl;
      readonly id: number;
      readonly type: AffirmationType;
    }
    
    @Injectable({ providedIn: 'root' })
    export class AffirmationSectionRepository {
      private _reset$ = new ReplaySubject<void>(1);
      private _save$: Observable<void>;
      constructor(private _service: CorporateGovernanceService) {}
    
      /**
       * Questions (config) of affirmations
       */
      readonly questions$ = this._reset$.pipe(
        switchMap(() => this._service.getAffirmationsConfig()),
        shareReplay(1)
      );
    
      /**
       * Answers (data) of afirmations
       */
      readonly answers$ = this._reset$.pipe(
        switchMap(() =>
          forkJoin([
            this.questions$.pipe(take(1)),
            this._service.getAffirmationsData(APP_ID),
          ])
        ),
        map(([affirmationCfgs, data]) => {
          const dataById = keyBy(data, (x) => x.affirmationQnsId);
    
          return flatMap(affirmationCfgs, (cfg) =>
            cfg.affirmationQns.map((q) => {
              const { compalianceStatus: status, affirmationDataId: id } =
                dataById[q.affirmationQnsId] ?? {};
    
              return {
                ...q,
                section: cfg.affirmationName,
                type: cfg.affirmationType,
                answer: new FormControl(status ? status === 'Y' : null),
                id: id ?? null,
              } as AffirmationRow;
            })
          );
        }),
        shareReplay(1)
      );
    
      /**
       *
       * @returns progress observable
       */
      save() {
        return (
          this._save$ ||
          (this._save$ = this.answers$.pipe(
            take(1),
            filter((rows) => some(rows, (x) => x.answer.dirty)),
            map((rows) =>
              rows
                .filter((row) => isBoolean(row.answer.value))
                .map(
                  (row) =>
                    ({
                      applicationId: APP_ID,
                      affirmationDataId: row.id,
                      affirmationQnsId: row.affirmationQnsId,
                      compalianceStatus: row.answer.value ? 'Y' : 'N',
                    } as AffirmationData)
                )
            ),
            switchMap((data) => this._service.saveAffirmationsData(data)),
            withLatestFrom(this.answers$),
            tap(([_, rows]) => forEach(rows, (x) => x.answer.markAsPristine())),
            mapTo(void 0),
            shareReplay(1),
            finalize(() => (this._save$ = null))
          ))
        );
      }
    
      reset() {
        this._reset$.next();
      }
    }

我需要 100% 的覆盖率。但我不明白如何覆盖问题$、answers$ 和 save() 函数。

这是当前的实现和结果

import { TestBed, fakeAsync } from '@angular/core/testing';
import { of, Observable, ReplaySubject, BehaviorSubject } from 'rxjs';
import { AffirmationSectionRepository } from './affirmation-section.repository';
import { CorporateGovernanceService } from 'src/app/services/api/corporate-governance.service';
import { Affirmation, AffirmationData, AffirmationType } from 'src/app/models/affirmation';
import { AppService } from 'src/app/services/components/app.service';
import { TsMessageType } from 'src/app/components/message/message-item/message-item.component';

describe('GroupsService', () => {
    let service: AffirmationSectionRepository;

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                { provide: CorporateGovernanceService, useClass: MockCorporateGovernanceService },
                { provide: AppService, useClass: MockAppService },
            ]
        });
        service = TestBed.inject(AffirmationSectionRepository);
    });

    it('should be created', () => {
        expect(service).toBeTruthy();
    });

    it('should call save method', () => {
        service.save().subscribe(x => {
            expect(x).toBeTruthy();
        });
        expect(service.save()).toEqual((service as any)._save$);
        expect(service).toBeTruthy();
    });

    it('should call save method', () => {
        (service as any)._save$ = new ReplaySubject<void>(1);
        service.save();
        expect(service.save()).toEqual((service as any)._save$);
        expect(service).toBeTruthy();
    });

    it('should call reset method', () => {
        (service as any)._reset$ = new ReplaySubject<void>(1);
        service.reset();
    });
});





class MockCorporateGovernanceService {
    getAffirmationsConfig(): Observable<Affirmation[]> {
        return of([
            {
                affirmationId: 1,
                affirmationName: 'name',
                isActive: true,
                affirmationQns: [],
                affirmationType: AffirmationType.Normal
            }
        ]);
    }

    getAffirmationsData(applicationId: number): Observable<AffirmationData[]> {
        return of([
            {
                affirmationDataId: 1,
                affirmationQnsId: 1,
                applicationId: 1,
                compalianceStatus: 'Y'
            } as AffirmationData
        ]);
    }

    saveAffirmationsData(data: AffirmationData[]): Observable<void> {
        return of();
    }
}


class MockAppService {
    appId$ = new BehaviorSubject<number>(1);
    open(type: TsMessageType, message: string): void { }
}

【问题讨论】:

  • 出于某种原因,它们可能是私有的和只读的。您可能应该看看嘲笑CorporateGovernanceService,然后改为调用公众reset()
  • @FrankFajardo 我已经将其称为最后一个测试用例。那是什么错误?
  • 你需要再写一个单元测试来保存,这里你不会定义(service as any)._save$ = new ReplaySubject(1);
  • 是否必须在questions$answers$ 发出某些东西之前调用reset()

标签: angular unit-testing jasmine karma-jasmine


【解决方案1】:

您正在替换/覆盖 _reset$_save$ 中的私有 Subjects/Observables(questions$answers$ 依赖)在 questions$answers$ 被初始化之后 save() 可以分配之前他自己的实现(第一次调用时)。

因此管道运算符永远不会执行,因为发出的 Observable 是来自您的测试的,而不是在 AffirmationSectionRepository 中创建的。

另外,就像 Frank 提到的那样,您至少需要模拟 getAffirmationsData()saveAffirmationsData()


编辑:更新为什么测试不涵盖第 43 行

第 43 行在_reset$ 发出事件 有一个活动订阅questions$ 时执行。

questions$ 的唯一消费者是来自 answers$forkJoin(在第 53 行)。由于take(1),它只消费一次(所以在第一个事件之后它在内部被取消订阅)。

answers$ 再次需要有一个有效的订阅,这里应该来自save() 的第一次调用(第 85 行)。

所以你必须调用save(),订阅结果,然后调用reset()(这可能是你的“缺失链接”)。

【讨论】:

  • 我不清楚你的第二点。您能否检查我的实施并提出遗漏的建议?
  • 关于嘲讽?我认为您对问题的编辑对MockCorporateGovernanceService 的更改应该可以做到。
  • 但是您对规范的更改似乎很麻烦。
  • 你能帮我看看缺少的东西吗?它永远不会执行第 43 行 this._service.getAffirmationsConfig() 我还没有覆盖 _reset$
  • 查看我的更新。也许在订阅save() 的结果后调用reset() 是缺少的。
猜你喜欢
  • 2016-05-07
  • 2017-03-21
  • 2013-02-12
  • 2019-07-09
  • 1970-01-01
  • 1970-01-01
  • 2019-05-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多