I would like to test a GraphQL subscription in Angular that
- Make a query
- Attach a subscription to the query by using
subscribeToMore
- Mock the query result by using flush on its operation in and validate the result
- 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 ?