Using Stubs and Mocks in Jasmine to test your Angular code

Setting up your environment and is a very important part of unit testing AngularJS code. We discuss some of the tools and methods under which we can provide the full workflow of a system under test in this post.

Intro

Unit tests allow us to automatically test our code under a variety of conditions to prove that it works and when it breaks it does so in expected ways. In order to predictably test code, we also need to completely control the setup and data provided to the code under test.

Thankfully, the test tools provided for Angular testing allow you to mimic your models and control how your code responds to code in a very precise way. Jasmine provides some easy way to create test doubles and even “spy” on their execution.

In this post we will discuss some of the best practices and tools we use to control the environment your code is working against.

Why do we need to fake stuff?

We need to control the entire environment and workflow to make sure we are testing the exact scenarios.

Messing around with the DB and creating fake records can be difficult at best, at worst it can create false positives. Even more common, some CI systems lack access to a DB at all

We also need the ability to re-create the conditions under which a bug has been created. sometimes this means we need to replicate some crazy conditions

There are two tools to make this happen: Test Stubs (also known as  Fakes, depending on who you ask) and Mock Objects:

  • Test stubs are a very simplified object or data structure designed specifically for the test scenario. These can be constructed at the beginning of the test or per test.
  • Mock objects are class instances that mimic an existing class to provide the same method interface and return specific values when a method call occurs.

Test Stubs in AngularJS & Jasmine

Test stubs are a simple data structure or model we rely on when running our tests. These can be as simple as a static array of data or a very lightweight object with publically scoped methods. To differentiate a stub from a mock, we typically only mimic the methods we are actually testing. This is quite useful during

More often than not a stub is created at the beginning of the test suites and made accessible to the suite’s tests. A good practice is to limit the modifications to the stubs to make sure you are always testing the same thing.

If you must make changes it is a good idea to make a local copy to isolates your modifications to a specific test.

Test Stub Examples

HeaderComponent

Below is an abridged version of the component code we are unit testing. The important thing to note: at the end of this method’s execution we make a call to the router service to navigate the user to the “login” route and we need to make sure this and only this method is executed.

/**
 * Simple component providing a navigation header.
 */
...
export class HeaderComponent implements OnInit {
    ...
    /**
     * clickLogout clears the user's creds but also 
     * takes the user to the Login route
     */
    clickLogout() {
        ...
        this.router.navigate(['/login']);
    }
    ...
}

Test Suite Code

Here is the abridged test suite where we create our test stub

...

/** 
 * beforeEach setup executes before each test suite test runs.
 *
 * This allows us to setup the stub before each test
 */
beforeEach(async(() => {

    ...
    
    TestBed.configureTestingModule({
        ...
        providers: [

        /**
         * Create a very basic stub object with one method:'navigate'
         *
         * Use Jasmine's createSpy to create a very basic function
         * which also allows us to "listen in" when it's called
         */
        {
            provide: Router,
            useClass: class { 
                navigate = jasmine.createSpy("navigate"); 
            }
        }
        ...
    ]})
    .compileComponents();

    ...

}));

/**
 * Here we test the method and make sure we actually navigate
 */
it('should navigate to /login when clickLogout is fired', () => {
   ...
   let router = fixture.debugElement.injector.get(Router);
   component.clickLogout();

   // "listen" to make sure that the navigate method has been 
   // called and it was called with the expected value
   expect(router.navigate).toHaveBeenCalledWith(["/login"]);
   ...
});

 

Mocks

A Mock object is a simulated object instance used to mimic a classes behavior using the same interface. In simpler terms, this is a fake class with the same method signature as the Real Thing.

A mock object can be composed of multiple objects (and sometimes multiple mock objects), but most often should be created as simply as possible to keep your tests easily maintained.

Mock Object Example

/**
 * Create a mock of an existing service
 * by simply extending it and overriding some 
 * of the methods you wish to use in your tests
 */
class MockAuthService extends AuthService {

    /**
     * This method is implemented in the AuthService
     * we extend, but we overload it to make sure we
     * return a value we wish to test against
     */
    isLoggedIn() {
        return false;
    }
}

...

beforeEach(async(() => {

    ...

    TestBed.configureTestingModule({

        ...

        providers: [

        /**
         * Inject our mocked service in place of AuthService
         */
        {
            provide: AuthService,
            useClass: MockAuthService
        },

        ...

    ]})
    .compileComponents();

    ...

}));


it('should navigate to /login when clickLogout is fired', () => {

   /**
    * Get the mocked service here from our fixture
    * and add a spyOn over-ride to pretend we have
    * a logged in user.
    */
   let service = fixture.debugElement.injector.get(AuthService);
   spyOn(service, 'isLoggedIn').and.returnValue(true);
   
});

Conclusion

Unit tests are only as good as the environment you can provide for your code under tests. It’s very important to be able to mimic and recreate the environment under which you have both positive and negative results for your code.

Unit Testing AngularJS Components & Services

Unit testing is a very important part of software development, and it is extremely important to Angular applications as they become increasingly more complex. We’ll explain how to set up your unit tests and describe what you can actually test with some examples.

Intro

Unit testing is a very important part of the software development process, especially as your applications become more and more complex. Making sure your code is robust and adaptable is an important aspect of being professional. This is no less true with your AngularJS application.

If you are reading this, you are most likely already sold on unit testing but here’s some benefits and reasons for writing and maintaining your unit tests:

  • The ability to quickly update your code and make sure you are able to regressively test your code.
  • Updating your unit tests to cover a fixed bug will prevent the bug from getting re-introduced!
  • Use a CI to make sure EVERYONE on your team is testing the code.

Angular CLI, Jasmine & KarmaJS

Here at Arroyo Labs we use the latest version of Angular CLI a lot. It’s a great way to start a project very quickly provides a great start for your application. It also gives you some boilerplate unit tests for each component and service you create using the CLI.

While we have explained Jasmine and KarmaJS in past blog articles, here is a quick overview explaining how these frameworks work together:

  • Jasmine
    • A very popular behavior driven Javascript testing framework with a very clean syntax.
  • KarmaJS
    • A framework that allows you to run your Jasmine tests on a device or headless browser. The test runner itself will provide a DOM with which your code is rendered and provides the API that Jasmine will interact with.

Setting up your tests

To create a unit test, you need to include a consistent environment in which to run your code under test and we need to control the required classes and variables that go into creating this environment. Thanks to the magic of dependency injection, we can mock up some classes used by our classes and code.

Let’s take a look at the ‘top’ portion of a unit test we use in our user-admin project (you can find the whole unit test in our repo):

/**
 * Import some basic testing classes and utilities from AngularJS's
 * core testing code. This is the minimum required code to create 
 * a basic unit test.
 */
import {
     async,
     getTestBed,
     TestBed
} from '@angular/core/testing';

/**
 * Import some specific test classes used to mock the 
 * http backend and connection classes
 */
import {
    MockBackend,
    MockConnection
} from '@angular/http/testing';

/**
 * Import the HTTP classes we use in our service
 */
import {
    BaseRequestOptions,
    Http,
    Response,
    ResponseOptions,
    XHRBackend
} from '@angular/http';

/**
 * Finally, we import the required custom classes we use in our service,
 * and the service we are testing as well.
 */
import { User }         from '../shared/models/user.model';
import { AuthService }  from './auth.service';

import { UsersService } from './users.service';

What should we test in a service?

To test a service, we have to understand the underlying concept they represent: isolating logic and code into a re-usable class. This means we have a class we can pass around in our application where we get and manipulate data in expected ways.

Keep in mind, we can really only test publically scoped variables and functions. The unit test itself is really only able to interact with your service in the same way that an application is able to.

So, we have these things to test:

  • What public variables get initialized when we create the service?
  • Do public variables update as we expect when we call public functions?

Example Service Unit Test: UserService

In our UserAdmin project, we have a service called UserService used to interact with and manipulate user data we get from the AJAX backend.

The service itself only has quite a few public methods (it’s a very important class!) so we will focus on just two of the suite’s tests of the same method `getUsers`:

getUsers should return an empty observable list when the ajax request is unsuccessful

This is a test to make sure we return an empty list of users if the AJAX response does not contain an encoded list of users. This is a negative test, we are making sure that we return an expected response even when the backend returns an error.

it('#getUsers should return an empty observable list when the ajax   request is unsuccessful', () => {

 // set up the mocked service to return an
 // unsuccessful response (no users, 500 status)
 // with a helper method
 usersBodyData.success = false;
 setupConnections(backend, {
     body: {
         body: usersBodyData // use the previously init'd var for consistent responses
     },
     status: 500
 });

 // set up our subscriptions to test results 
 // when we actually get a result returned
 service.users$.subscribe((res) => {
     if(res) {
         expect(res).toBeTruthy();
         expect(res.length).toEqual(0);
     }
 });

 service.total$.subscribe((res) => {
     if(res) {
         expect(res).toBeTruthy();
         expect(res).toEqual(0);
     }
 });

 // make the actual request!
 let res = service.getUsers();

});

getUsers should return an observable list of users and result total when the ajax request is successful

This test makes sure that we get an observable list of users if the AJAX response is successful. To make sure this happens, we mock the response as though we have a JSON encoded list of users.

it('#getUsers should return an observable list of users and result total when the ajax request is successful', () => {
 // set up the mocked service to return an
 // unsuccessful response (users, 200 status)
 // with a helper method
 setupConnections(backend, {
 body: {
 body: usersBodyData
 },
 status: 200
 });
 
 // set up our subscriptions to test results 
 // when we actually get a result returned
 service.users$.subscribe((res) => {
 if(res) {
 expect(res).toBeTruthy();
 expect(res.length).toEqual(2);
 }
 });
 
 service.total$.subscribe((res) => {
 if(res) {
 expect(res).toBeTruthy();
 expect(res).toEqual(2);
 }
 });
 
 // make the actual request, with some params to pass via query string
 // we will explore this in a future post
 let res = service.getUsers(42, 42, 'id', 'desc');
});

What should we test in a component?

A component, like a service, is also a re-usable piece of code but it also has a defined lifecycle from its parent class and a frontend element via it’s template and optional CSS.

So, here is what we have to test:

  • Does this component actually get constructed?
  • What public variables get initialized and what DOM elements get rendered when we create the component?
  • Do public variables update as we expect when we call public functions?

Like we have noted above when discussing testing a service, you can really only test things that have been scoped as public by the component itself. This does include rendered DOM elements and you should test that these elements appear and render as expected since these are made public by the component itself.

Example Component Unit Test: UserListComponent

Test that the component actually results in a rendered component

it('should create', () => {
   fixture.detectChanges();
   const compiled = fixture.debugElement.nativeElement;
   component.ngOnInit();
 
   component.users = [];
   component.total = 0;
 
   // do we have a component at all?
   expect(component).toBeTruthy();
 
   // create new user button
   expect(compiled.querySelector('.btn-info')).toBeTruthy();
 
   // do we have a table
   expect(compiled.querySelector('table')).toBeTruthy();
   expect(compiled.querySelectorAll('tr').length).toBe(2);
});

Test that we display a list of users with a mocked response

it('should display list of users', () => {
   fixture.detectChanges();
   const compiled = fixture.debugElement.nativeElement;
 
   setupConnections(backend, {
       body: {
           body: bodyData
       },
       status: 200
   });
 
   component.ngOnInit();
 
   // create new user button
   expect(compiled.querySelector('.btn-info')).toBeTruthy();
 
   fixture.detectChanges();
 
   // do we have a table with users?
   fixture.detectChanges();
   expect(compiled.querySelectorAll('tr').length).toBe(21);
 
   // do we see the expected page count
   expect(component.getPageCount()).toBe(2); 
});

Conclusion

Unit testing is a not a oft-celebrated portion of the software development cycle, but it is an important part of the process. Once you have good tests in place, you can pivot direction and update your code with an increased sense of security.