How to use rxjs marbles testing with graphQL Apollo Testing Controller in Angular

602 Views Asked by At

I would like to test a GraphQL subscription in Angular that

  1. Make a query
  2. Attach a subscription to the query by using subscribeToMore
  3. Mock the query result by using flush on its operation in and validate the result
  4. Mock the subscription result by using flush on its operation and validate the result

I succeed to make a good test by following the Apollo documentation about client testing:

const ENTITY_QUERY = gql`
  query EntityList {
    entityList {
      id
      content
    }
  }
`;

const ENTITY_SUBSCRIPTION = gql`
  subscription OnNameChanged {
    nameChanged {
      id
      name
    }
  }
`;

describe('Test Subscription', () => {
  let backend: ApolloTestingController;
  let apollo: Apollo;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ApolloTestingModule],
      providers: [
        {
          provide: APOLLO_TESTING_CACHE,
          useValue: new InMemoryCache({ addTypename: true })
        }
      ]
    });
    backend = TestBed.get(ApolloTestingController);
    apollo = TestBed.get(Apollo);
  });

  it('should subscribe and return updated entity', done => {

    const queryRef: QueryRef<any> = apollo.watchQuery({ query: ENTITY_QUERY });

    queryRef.subscribeToMore({
      document: ENTITY_SUBSCRIPTION,
      updateQuery: (entityList, { subscriptionData }) => ({
        entityList: entityList.map(entity => {
          // update name of corresponding entity in cache
          return entity.id === subscriptionData.data.nameChanged.id
            ? {
                ...entity,
                name: subscriptionData.data.nameChanged.name
              }
            : entity;
        })
      })
    });

    const queryResult = [{ id: '1', name: 'John' }, { id: '2', name: 'Andrew' }];
    const subscriptionResult = { id: '1', name: 'Marc' };

    const expectedEntitiesWhenQuery = queryResult;
    const expectedEntitiesAfterSubscriptionUpdate = [subscriptionResult, { id: '2', name: 'Andrew' }];

    let firstQueryTick = true;

    // the subscription should be done before the flush in other case the operation backends would not be available
    queryRef.valueChanges.subscribe(result => {
      try {
        if (firstQueryTick) {

          // first test the query result returns the expected
          expect(result).toEqual(expectedEntitiesWhenQuery);

          firstQueryTick = false;

        } else {

          // then, when the subscription return a new name, test that the result is modified
          expect(result).toEqual(expectedEntitiesAfterSubscriptionUpdate);

          done();
        }
      } catch (error) {
        fail(error);
      }
    });

    // retrieves the query operation backend
    const backendSubscription = backend.expectOne('OnNameChanged');

    // retrieves the subscription operation backend
    const backendQuery = backend.expectOne('EntityList');

    // Mock by flushing data to query
    backendQuery.flush({ data: { entityList: queryResult } });

    // Then Mock by flushing data to subscription
    backendSubscription.flush({ data: { nameChanged: subscriptionResult } });
  });

  afterEach(() => {
    backend.verify();
  });
});

But as you can see, the validation of the results in the subscribe part isn't really clean with the firstQueryTick variable... just imagine if I want to test 10 results...

So I tried to replace this part by using the rxjs marble testing:

import { APOLLO_TESTING_CACHE, ApolloTestingController, ApolloTestingModule } from 'apollo-angular/testing';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { TestBed } from '@angular/core/testing';
import { Apollo, QueryRef } from 'apollo-angular';
import gql from 'graphql-tag';
import { TestScheduler } from 'rxjs/testing';

const ENTITY_QUERY = gql`
  query EntityList {
    entityList {
      id
      content
    }
  }
`;

const ENTITY_SUBSCRIPTION = gql`
  subscription OnNameChanged {
    nameChanged {
      id
      name
    }
  }
`;

describe('Test Subscription', () => {
  let backend: ApolloTestingController;
  let apollo: Apollo;
  let scheduler: TestScheduler;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ApolloTestingModule],
      providers: [
        {
          provide: APOLLO_TESTING_CACHE,
          useValue: new InMemoryCache({ addTypename: true })
        }
      ]
    });

    backend = TestBed.get(ApolloTestingController);
    apollo = TestBed.get(Apollo);

    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  it('should subscribe and return updated entity', done => {
    const queryRef: QueryRef<any> = apollo.watchQuery({ query: ENTITY_QUERY });
    queryRef.subscribeToMore({
      document: ENTITY_SUBSCRIPTION,
      updateQuery: (entityList, { subscriptionData }) => ({
        entityList: entityList.map(entity => {
          // update name of corresponding entity in cache
          return entity.id === subscriptionData.data.nameChanged.id
            ? {
                ...entity,
                name: subscriptionData.data.nameChanged.name
              }
            : entity;
        })
      })
    });

    const queryResult = [{ id: '1', name: 'John' }, { id: '2', name: 'Andrew' }];
    const subscriptionResult = { id: '1', name: 'Marc' };


    /////////////////////////////NEW PART

    scheduler.run(({ expectObservable }) => {
      // the source is the query observable
      const source$ = queryRef.valueChanges;

      const expectedMarble = 'x-y|';
      const expectedValues = { x: queryResult, y: [subscriptionResult, { id: '2', name: 'Andrew' }] };

      // this is subscribing and validating at the same time so it is not possible to do something between the subscription and the flush of values from rxjs
      expectObservable(source$).toBe(expectedMarble, expectedValues);
    });

    /////////////////////////////

    // this will not be called because the test is already failing with expectObservable, if we put this part before, it will fail because the subscription is not already done...
    const backendSubscription = backend.expectOne('OnNameChanged');
    const backendQuery = backend.expectOne('EntityList');

    backendQuery.flush({ data: { entityList: queryResult } });
    backendSubscription.flush({ data: { nameChanged: subscriptionResult } });
  });

  afterEach(() => {
    backend.verify();
  });
});

After multiple tries, I cannot make it works because the expectObservable is doing the subscribe + the validation at the "same time" and I need to:

  • FIRST subscribe to the query
  • THEN get the operations backend object to be able to flush data
  • THEN validate results

Is it possible to make an action between the subscribe and the validation using rxjs testing marbles ?

0

There are 0 best solutions below