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.
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 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.
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']);
}
...
}
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"]);
...
});
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.
/**
* 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);
});
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.
Tags: angular, jasmine, karmajs, mock, stub, testing, unittesting
Categories: Miscellaneous, TypeScript
Lets talk!
Join our mailing list, we promise not to spam.