Beginner’s Guide to Ionic Angular Unit Testing (Part 2) — Mocks and Spies

Abhishek Rathore
Enappd
Published in
8 min readAug 24, 2020
Beginner’s Guide to Ionic Angular Unit Testing (Part 2) — Mocks and Spies

In our first part of this series, we saw how to set up the basic testing environment and got familiar with some of the terminologies for Unit Testing in the Angular world. Now you should be able to know — Karma, Jasmine tests, TestBed, Unit Testing: Mocks & Stubs etc.

In this part, we will move further and add some more functionality to our app with the help of Angular Services(Testing services). As service dependencies will be injected into the components they will increase the complexity of our tests. Let’s jump ahead to the coding part.

We have added some code in a new Service called ApiService — this service will be used by our page to add jobs and store existing jobs. As you know in any complex application, services should have the business logic and components should do only view update related stuff. So we prefer to use Services as basic building blocks for business logic

The above code also has an AlertComponent, which we will use to show that job has been added to the Job List successfully.

Now let’ see how our component/page code looks like :

Very simple code. this.jobs still will have the job array — through the binding to service variable — But now data is stored in the service. addJob function has the same structure except for the logic — which has been moved to the service.

At this moment if you will run your tests they will pass! Surprisingly 😮

As we only tested jobs in our previous tests - which is still the same and function calls also remain the same.

However, now I want to add a more deep test which will check if addJob function of apiService is called or not. This function is now doing the main job of storing the data in jobs array. Now let us move forward in Angular testing using Spies by looking at testing with mocks & spies.

SPIES in Testing🕵

For checking whether a function is called we create something called SPY. This spy is actually a fake implementation and it runs in place of the actual function. Test spy is an object that records its interaction with other objects all round the code base.

So we need something like this in our test code

spyOn(apiService, 'addJob');

The above statement means the object apiService is the parent object which has addjob target function. Only this target functions will be spied upon.

But wait !! how we got this apiService in the Test file? We don’t have discussed any concept of service injection in Test files yet.

Here’s how we can inject the apiService in our TestBed (for testing services with the TestBed) — very similar to as we do in the module. Under the providers

TestBed.configureTestingModule({
declarations: [Tab1Page],
imports: [IonicModule.forRoot(), ExploreContainerComponentModule],
providers: [ ApiService ]
}).compileComponents();

However, I will not like to add the real Service here — WHY ?? 🤔

The reason is that your app may get too complicated and we might not want to understand what service is doing. We just want to test our component’s code independently.

So we MOCK our service also.

After having a closer look at test spies let us move to mocking with fake classes.

MOCKS in Testing 🐵

Mocks or Stubs as they are rightly called — represent a fake object or class which contains similar signature (objects and methods) to that of the original class. These functions may not do anything but their fake implementation is helpful in testing the unknown part of code.

Most people actually refer to Test Doubles while talking about Mocks. A Test Double is simply another object that can be passed in its place, which is similar to the interface of the required Collaborator.

Let us see mocking in testing. For example, in our component testing, we don’t need to know what getJobs is doing and how. We just know that it returns an array so we can stub that method in our Mock.

Here is an example of ApiServiceStub :

const ApiServiceStub = {
addJob: () => null,
getJobs: () => []
};

Yes, it does nothing but returns something similar to what actual function returns. Now let’s use this Stub/Mock to create a fake ApiService in our test file. We will use providers again to inject this. However this time we will use

providers: [{ provide: ApiService, useValue: ApiServiceStub },]

Above Syntax can read as ApiService created from an Object value ApiServiceStub. In Place of useValue you can use another syntax useClass sometimes — when you have a Class for the Mock/Stub.

So now we have a Service ApiService also available in our Test file. But as we do it in components, we will instantiate a new object from Service. In components we use constructor this — here we will use TestBed’s inject method. We will add a line :

apiService = TestBed.inject(ApiService)

Now let’s see our completed tests file. Look at how we added Service file, how we instantiated it, and how we changed the Last Test :

If you check the last test we added a SPY and also we expected that those functions will be called. We are using toHaveBeenCalled and toHaveBeenCalledWith to check if the Spied function was called. The two functions toHaveBeenCalled and toHaveBeenCalledWith only differ slightly. The later one also checks whether the argument passes also matches. We have just used both to showcase the difference. In the above scenario, You can only have to use the second one toHaveBeenCalledWith — as it tells whether functions are being called and also the arguments.

If you run the test now — it will have a failure. And failure is in the Test which was passing earlier. WHY ?? Any guesses?

As you have stubbed the service with fake implementation — your component is always using the fake implementation and you are always getting an empty array in return from getJobs . Above test expected more than 1 element.

Now you know testing is hard if you don’t think of side-effects.

So, I will not inject this Mock Service globally and inject it in a specific case only where it is needed. I am using Testbed.OverrideProvider in this case.

But in real-life testing, you will want to have the only stub and not real service at all. So above ugly workaround is not something to be proud of.

Let’s rethink our code how we can make all tests work with Stubbed Service. Can there be a way that our fake getJobs return something meaningful? The answer is YES

SPY methods

Till now we just used SpyOn, to test the component logic, to create a SPY for a function. But that spy was not doing anything expect capturing the call to function. However, we have a lot of methods available on SpyOn output object — which can provide some life to our spied on methods. These are :

  • returnValue — this returns a value you mention from the spied method
  • callFake — this provides a way to implement some fake function to be called when it is spied. You can even replace it will the code of the original method. But mostly a bad idea as testing should be simpler.
  • callThrough — this is also a way in which you still want to use the SPY but leave the functionality un-touched and the original method can be called. In the case of our Test file — this original method is still not the one in service method — it is the one in the Stub Object - getJobs . So we can’t use it here.

Let’s use the easiest one returnValue

it('addJob should add the job string to jobs array', () => {const job = 'Dummy Job';
spyOn(apiService, 'addJob');
spyOn(apiService, 'getJobs').and.returnValue([job]);
component.addJob(job);
expect(component.jobs.length).toBeGreaterThan(0);
expect(component.jobs).toContain(job);
});

So we have spied on both functions addJob and getJobs . However, getJobs now is returning the value of an array [job] . Yes, we have hardcoded the value here — but that’s ok for testing components, as we already believe services are working fine, they will be tested separately.

Now we have to change the file again — stub providers only at the top & no ugly overrides. However, there will be still a problem as getJobs will run even before the spies work. Because it is written in the constructor.

We can use ngOnInit which is more flexible and its call can be controlled in the Testing environment by

fixture.detectChanges();

This line controls the angular component loading activity and also calls ngOnInit . So we will remove this line from the common section of beforeEach and call only in the function we need to have ngOnInit called.

Carefully see the line where we added fixture.detectChanges() — Just after putting the spies. Be careful about placing the things in the test. If you put spies after the action — they will not act properly.

Now again let’s run tests.

All passing !!

But, we did cheating here. We have just passed something in getJobs which was not called sent by addJob — this means it’s not checking the actions of addJob.

So can we do better? Let’s change the code a little bit.

const jobs = [];spyOn(apiService, 'addJob').and.callFake((jb)=>{
jobs.push(jb);
});
spyOn(apiService, 'getJobs').and.returnValue(jobs);

Now you can see — jobs is the common array accessed by both getJobs and addJob . Also, we have used callFake which give us more power to change the functionality of addJob functions.

Test Again.

All Passing !! 😇

Stay tuned for Part 3 of this series !

Next Steps

If you liked this blog, you will also find the following blogs interesting and helpful. Feel free to ask any questions in the comment section

Ionic React Full App with Capacitor

If you need a base to start your next Ionic 5 React Capacitor app, you can make your next awesome app using Ionic 5 React Full App in Capacitor

Ionic 5 React Full App in Capacitor from Enappd
Ionic 5 React Full App in Capacitor from Enappd

Ionic Capacitor Full App (Angular)

If you need a base to start your next Angular Capacitor app, you can make your next awesome app using Capacitor Full App

Capacitor Full App with huge number of layouts and features
Capacitor Full App with huge number of layouts and features

Ionic Full App (Angular and Cordova)

If you need a base to start your next Ionic 5 app, you can make your next awesome app using Ionic 5 Full App

Ionic Full App with huge number of layouts and features
Ionic Full App in Cordova, with huge number of layouts and features

--

--