web-dev-qa-db-ja.com

コンポーネントでngrxセレクターをモックする方法

コンポーネントでは、ngrxセレクターを使用して状態のさまざまな部分を取得します。

public isListLoading$ = this.store.select(fromStore.getLoading);
public users$ = this.store.select(fromStore.getUsers);

fromStore.methodは、ngrx createSelectorメソッドを使用して作成されます。例えば:

export const getState = createFeatureSelector<UsersState>('users');
export const getLoading = createSelector(
  getState,
  (state: UsersState) => state.loading
);

テンプレートでこれらのオブザーバブルを使用します。

<div class="loader" *ngIf="isLoading$ | async"></div>
<ul class="userList">
    <li class="userItem" *ngFor="let user of $users | async">{{user.name}}</li>
</div>

私は次のようなことができるテストを書きたいです:

store.select.and.returnValue(someSubject)

サブジェクトの値を変更し、これらの値に対してコンポーネントのテンプレートをテストできるようにします。

事実、私たちはそれをテストする適切な方法を見つけるのに苦労しています。コンポーネントでselectメソッドが2つの異なるメソッド(MemoizedSelector)を引数として2回呼び出されるため、「andReturn」メソッドを記述する方法は?

実際のセレクターを使用したくないため、状態をモックしてから実際のセレクターを使用するのは適切な単体テスト方法ではないようです(テストは分離されず、実際のメソッドを使用してコンポーネントの動作をテストします)。

12
BlackHoleGalaxy

私は同じ課題にぶつかり、セレクターをサービスにラップすることでそれを一度解決したので、コンポーネントはストアを直接通過するのではなく、サービスを使用してデータを取得しました。これにより、コードがクリーンアップされ、テストが実装に依存せず、モックがはるかに簡単になりました。

mockUserService = {
  get users$() { return of(mockUsers); },
  get otherUserRelatedData$() { return of(otherMockData); }
}

TestBed.configureTestingModule({
  providers: [{ provide: UserService, useValue: mockUserService }]
});

しかし、それをする前に、あなたの質問の問題を解決しなければなりませんでした。

解決策は、データを保存する場所によって異なります。 constructorに保存する場合:

constructor(private store: Store) {
  this.users$ = store.select(getUsers);
}

その後、storeによって返される値を変更するたびに、テストコンポーネントを再作成する必要があります。それを行うには、次の行に沿って関数を作成します。

const createComponent = (): MyComponent => {
  fixture = TestBed.createComponent(MyComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
  return component;
};

そして、あなたのストアスパイが返すものの値を変更した後にそれを呼び出します:

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    const cmp = createComponent();
    // proceed with assertions
  });
});

または、ngOnInitに値を設定している場合:

constructor(private store: Store) {}
ngOnInit() {
  this.users$ = this.store.select(getUsers);
}

コンポーネントを1回作成すると、ストアからの戻り値を変更するたびにngOnInitを呼び出すだけで済むため、少し簡単になります。

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    component.ngOnInit();
    // proceed with assertions
  });
});
6
vince

私はそのようなヘルパーを作成しました:

class MockStore {
        constructor(public selectors: any[]) {
        }

        select(calledSelector) {
          const filteredSelectors = this.selectors.filter(s => s.selector === calledSelector);
          if (filteredSelectors.length < 1) {
            throw new Error('Some selector has not been mocked');
          }
          return cold('a', {a: filteredSelectors[0].value});
        }
 }

そして今、私のテストは次のようになります。

  const mock = new MockStore([
    {
      selector: selectEditMode,
      value: initialState.editMode
    },
    {
      selector: selectLoading,
      value: initialState.isLoading
    }
  ]);

  it('should be initialized', function () {
    const store = jasmine.createSpyObj('store', ['dispatch', 'select']);
    store.select.and.callFake(selector => mock.select(selector));

    const comp = new MyComponent(store);

    comp.ngOnInit();

    expect(comp.editMode$).toBeObservable(cold('a', {a: false}));
    expect(comp.isLoading$).toBeObservable(cold('a', {a: false}));
  });
2
Piotr Korlaga

また、この問題に遭遇し、セレクターをラップするためにサービスを使用することも私にとって選択肢ではありません。特にテスト目的だけでなく、サービスを置き換えるためにストアを使用しているためです。

したがって、私は次の(また完璧ではない)ソリューションを思いつきました。

私は、各コンポーネントと異なるアスペクトごとに異なる「ストア」を使用します。あなたの例では、次のStores&Statesを定義します。

export class UserStore extends Store<UserState> {}

export class LoadingStatusStore extends Store<LoadingStatusState> {}

そして、それらをUser-Componentに注入します。

constructor( private userStore: UserStore, private LoadingStatusStore: 
LoadingStatusStore ) {}

それらをUser-Component-Test-Class内でモックします。

TestBed.configureTestingModule({
  imports: [...],
  declarations: [...],
  providers: [
    { provide: UserStore, useClass: MockStore },
    { provide: LoadingStatusStore, useClass: MockStore }
  ]
}).compileComponents();

BeforeEach()またはit()テストメソッドにそれらを注入します:

beforeEach(
  inject(
    [UserStore, LoadingStatusStore],
      (
        userStore: MockStore<UserState>,
        loadingStatusStore: MockStore<LoadingStatusState>
      ) => {...}

次に、それらを使用して、さまざまなパイプメソッドをスパイできます。

const userPipeSpy = spyOn(userStore, 'pipe').and.returnValue(of(user));
const loadingStatusPipeSpy = spyOn(loadingStatusStore, 'pipe')
  .and.returnValue(of(false));

この方法の欠点は、1つのテスト方法でストアの状態の複数の部分をテストできないことです。しかし今のところ、これは私の回避策として機能します。

2
Tac

セレクターをサービスに移動しても、セレクター自体をテストする場合、セレクターをモックする必要はありません。 ngrxには独自のモック方法があり、ここで説明します。 https://ngrx.io/guide/store/testing

0
jur

次のようなものを使用できます。

spyOn(store, 'select').and.callFake(selectFake);

function pipeFake(op1: OperatorFunction<UsersState, any>): Observable<any> {
  if (op1.toString() === fromStore.getLoading.toString()) {
    return of(true);
  }

  if (op1.toString() === fromStore.getUsers.toString()) {
    return of(fakeUsers);
  }

  return of({});
}
0