Each test suite starts with a describe() statement. This describes the test suite, which is actually a function that includes one or more tests within. I define the service variable, which is accessible for all the following tests that will be written inside this function.
describe('Youtube Search Service', () => { let service: YoutubeSearch;
Chapter 5 ■ Understanding serviCes with reaCtive programming
Next, I use Jasmine’s beforeEach() life-cycle hook. It’s a function that is mostly used for setting up the required state for each test that is going to run inside this describe() suite that we have defined.
beforeEach(() => {
Now, we need to set up objects that we expect to check and run before we create the instance of the YoutubeSearch service. I’m going to use Jasmine’s “spy.”
A spy is a mocked-up object that includes special mocked-up functions that can be tracked by Jasmine. We can use spies to tell whether a function has been called, how many times it has been called, whether it has been called with parameters, and even whether it has been called with specific parameters.
I set up one spy for YoutubeApiService. I don’t want to actually test this service at this stage, but rather I need its API interface, since it’s being used inside the YouTube search class. I’m using the createSpyObj() function to create a spy object with those special functions that I can track.
I created a simple YoutubeApiFactory object as a replacement for the real class.
let youtubeApiServiceSpy = jasmine.createSpyObj('youtubeApiServiceSpy', [ 'setOptions', 'setConfig', 'isNewSearchQuery',
'setNextPageToken', 'resetPageToken' ] );
const youtubeApiFactory = {
create: () => youtubeApiServiceSpy };
The YoutubeSearch class uses the list() function in order to perform the actual search to YouTube’s API. I don’t really want to make this request each time I test, so I mock up this function, which should return an observable API-like object. Since I only need the map function from this API, this can be easily mocked up by creating a simple object with a map function that gets a function as a parameter and invokes it with a return value.
I do the same for the config property, which is an instance of Angular’s URLSearchParams. This class aids in creating a map-like object for URLsearch is a parameter. It exposes set and get methods for parameters, which are used inside the YoutubeApiService class.
youtubeApiServiceSpy.list = (val) => { return {
get: (q) => youtubeApiServiceSpy.config.q, set: (q) => youtubeApiServiceSpy.config.q = q };
Chapter 5 ■ Understanding serviCes with reaCtive programming
96
There is another way to spy on objects. Here, I’m attaching a spy to track calls of the isNewSearchQuery and resetPageToken functions of the YoutubeSearch class. I still want these functions to execute the code, so I use Jasmine’s .and.callThrough() to make sure these functions will be executed.
spyOn(YoutubeSearch.prototype, 'isNewSearchQuery').and.callThrough();
spyOn(YoutubeSearch.prototype, 'resetPageToken').and.callThrough();
The code that closes this beforeEach function creates the environment for this test.
This is where a module for testing purposes is created using the TestBed class. This is also where I instruct Angular to inject a different object instead of the actual implementation of YoutubeApiFactory. In this case, since YoutubeApiFactory is a class, Angular instantiates it by default using the new operator and then injects the instance. However, I use useValue in order to place the object youtubeApiFactory that I created before so Angular can just inject this object and skip the instantiation process.
TestBed.configureTestingModule({
imports: [ HttpModule ], providers: [
YoutubeSearch,
{ provide: YoutubeApiFactory, useValue: youtubeApiFactory }, ]
});
});
To conclude the testing environment setup process, we need to retrieve the actual YouTube-search instance. We can do that using the inject function that was imported.
This function does the same thing as the decorator @Inject() does; however, as of the time of writing, the decorator cannot be used when writing tests. Inject can be used in Jasmine’s beforeEach and it functions. It’s as if we inject this service in a component’s constructor.
An important note here: for keeping the code DRY and not repeating this injection in every test that follows, I chose to save the instance in the service variable that is accessible to all the tests inside this suite.
beforeEach(inject([YoutubeSearch], (youtubeSearch) => { service = youtubeSearch;
}));
I like to start by checking the constructor. In this case, the constructor should create an instance using YoutubeApiFactory. The actual expectation checks that the API property is defined.
it('should have an api instance', () => { const actual = service.api;
expect(actual).toBeDefined();
});
Chapter 5 ■ Understanding serviCes with reaCtive programming
Next, I want to make sure that the search method performs two important operations:
1. It should check whether the new query is different (new).
2. It should reset the page token when the query is new.
In this test, I’m using Jasmine’s toHaveBeenCalled() matcher (function) with the actual function calls to isNewSearchQuery and resetPageToken. This is where the spy I set earlier comes in handy.
it('should should check if query is new before search', () => { const actual = service.isNewSearchQuery;
service.search('ozrics');
expect(actual).toHaveBeenCalled();
});
it('should reset page token when query is new', () => { const query = 'ozrics';
service.search(query);
const actual = service.resetPageToken;
expect(actual).toHaveBeenCalled();
});
The last test that I want to show as an example of expressing a “false” expectation is for the searchMore functionality. This time, It should not set the next page token of the API property when the searchMore function is invoked while the YouTube search service is in “search” mode.
it('should NOT set the next page token when searching and asked to search more', () => {
const query = 'ozrics';
service.searchMore();
const actual = service.api.setNextPageToken;
expect(actual).not.toHaveBeenCalled();
});
This concludes the testing for the YouTube search service. Tests solve a huge headache while developing. Instead of running these scenarios manually over and over again, the process becomes automated and documented. Also, writing tests ensures we run all scenarios over and over again without skipping anything important. The quality of our application, its tests cycle, and the level of functionality significance are upgraded.
Seeing all the green checkmarks is a beautiful picture (Figure 5-6).
Chapter 5 ■ Understanding serviCes with reaCtive programming
98
Summary
This concludes the chapter on creating services and incorporating the power of RxJS. I talked about creating services of several perspectives, as follows:
1. Creating a reusable injectable service 2. Using a factory for creating an instance
3. Using a service as a “proxy” to interact with a third-party API 4. Introduction to ReplaySubject, a “recordable” special
observable
5. Writing tests for services
In the next chapter, I’m going to focus on the architecture side of the application—
organizing and simplifying the application’s code. As our application is getting bigger and bigger, we should keep an eye on how it is structured, which layers the architecture is composed of, and what the role of each layer is. I plan to minimize the responsibility and amount of code in components while introducing a new dedicated layer for mostly async operations as well as a bit of logic, which will be connected to the reactive flow that we have implemented thus far with the store.
Figure 5-6. Player is playing a media file, and it’s marked in the now-playing playlist
99
© Oren Farhi 2017