basics of Unit testing
- Why and why not write tests?
- Types of automated tests
- How to write good unit tests
- Code coverage
- UT Frameworks: Jasmine
- Testing Angular apps
Why and why not write unit tests
some perks of writing UT
Better understanding of the requirements
More robust components
Avoid bugs created by a refactor
Feature docs for developers
Less buggy continuous delivery
types of automated tests
end to end (e2e)
Term “End to End testing” is defined as a testing method which determines whether the performance of application is as per the requirement or not. It is performed from start to finish under real world scenarios like communication of the application with hardware, network, database and other applications.
End-to-end testing is a technique used to test whether the flow of an application right from start to finish is behaving as expected. The purpose of performing end-to-end testing is to identify system dependencies and to ensure that the data integrity is maintained between various system components and systems.
Integration tests
Integration testing (sometimes called integration and testing, abbreviated I&T) is the phase in software testing in which individual software modules are combined and tested as a group.
Integration testing is a software testing methodology used to test individual software components or units of code to verify interaction between various software components and detect interface defects. Components are tested as a single group or organized in an iterative manner.
Unit tests
Software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.
Is a level of software testing where individual units/ components of a software are tested. The purpose is to validate that each unit of the software performs as designed.
Unit tests
Effort to write & run
Reliability
UT
IT
E2E
How to write good unit tests
The structure of a UT
- Set up conditions for the test
- Trigger the method
- Verify the relevant results
- Undo modifications (clean up)
Some advice
Test you tests: Is it failing when it needs to fail?
Does it work consistently?
Name your tests correctly: It will help you focus on what you need to test.
One logical assertion per test: It will keep the tests simple enough.
Keep them unit: It's too tempting to avoid mocks, but it will make debugging harder when they broke.
Ensure good reports on fail
A good test fail report should give you information about:
- What were you testing?
- What should it do?
- What was the behaviour?
- What was the expected behaviour?
My function should return the result of adding the stringified numbers in the array, as a number FAILED
Expected 1 to be 8.
1
2
3
4
Code coverage
what is code coverage?
A measure used to describe the degree to which the source code of a program is executed when a particular test suite runs.
coverage is a lie
myFunction(stringsArray: string[]) {
return stringsArray
.map(numString => parseInt(numString, 10))
.reduce((prev, current) => prev + current);
}
it('is a non sense test', () => {
const goodArray = ['1', '3', '2'];
expect(this.service.myFunction(goodArray)).toBeDefined();
});
it('is and incorrect test', () => {
const badArray = ['1', '3', 'L'];
expect(this.service.myFunction(badArray)).not.toBe(jasmine.any(String));
});
coverage is useful
myFunction(stringsArray: string[]) {
return stringsArray
.map(numString => parseInt(numString, 10))
.filter(shouldBeNum => typeof shouldBeNum === 'number' ? shouldBeNum : 0)
.reduce((prev, current) => prev + current);
}
it('should add all the numbers in the array', () => {
const arrayToAdd = ['1', '3', '4'];
expect(this.service.myFunction(arrayToAdd)).toBe(8);
});
How much should i test?
There's no magic %, it's up to your project's needs.
In case of doubt, test it.
Ut Frameworks: Jasmine
why jasmine?
In this case, just because. Most UT frameworks are really similar, choose the one that fits you and your app.
- https://jasmine.github.io/ -> Fast and simple
- https://qunitjs.com/ -> Used by JQuery projects
- http://mochajs.org/ -> Faster for async tests (mocha.parallel)
- https://facebook.github.io/jest/ -> Easy layout testing
Basic testing
describe("Test suit name", function() {
var a;
it("should have at least one test", function() {
a = true;
expect(a).toBe(true);
a = undefined;
});
it("can have more than one", function() {
a = false;
expect(a).toBe(false);
a = undefined;
});
});
- Set up
- Trigger
- Verify
- Undo
Easier set up and undo
beforeEach and afterEach will run code before or after each test (it will run once per test).
beforeAll and afterAll will run code just once, before/after all the specs.
describe("Test suit name", function() {
var a;
afterEach(function() {
a = undefined;
})
it("will set a to undefined after this test", function() {
a = true;
expect(a).toBe(true);
});
it("will ALSO set a to undefined after this", function() {
a = false;
expect(a).toBe(false);
});
});
Easier set up and undo
this allows to share variables between beforeEach, afterEach and it.
It's reseted for each test.
describe("Test suit name", function() {
beforeEach(function() {
this.a = 'invalid'
})
it("should have the initial value", function() {
expect(this.a).toBe('invalid');
this.a = true;
expect(this.a).toBe(true);
this.b = false;
});
it("is reset between tests", function() {
expect(this.b).toBe(undefined);
});
});
Other Matchers
.not for negative assertions: expect(false).not.toBe(true)
.toEqual will compare the values of objects, but not their reference: expect(foo).toEqual(clonedFoo)
.toMatch for regular expresions: expect(message).toMatch(/bar/)
.toBeDefined, .toBeUndefined .toBeNull and .not.toBeNull are a fast way to pre-check if a variable or result is empty.
.toBeTruthy and .toBeFalsy for not boolean values:
expect(1).toBeTruthy(); expect(0).toBeFalsy()
.toThrow and .toThrowError for error management testing:
expect(foo).not.toThrow(); expect(foo).toThrow('F** you'); expect(foo).toThrowError('F** you')
spies
A spy will keep track of the calls made to a function, and can modify the result.
It only exists in the context where it's declared
describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {
foo = { setBar: function(value) { bar = value; }
};
spyOn(foo, 'setBar');
foo.setBar(123);
foo.setBar(456, 'another param');
});
it("tracks that the spy was called", function() {
expect(foo.setBar).toHaveBeenCalled();
});
it("tracks that the spy was called x times", function() {
expect(foo.setBar).toHaveBeenCalledTimes(2);
});
it("tracks all the arguments of its calls", function() {
expect(foo.setBar).toHaveBeenCalledWith(123);
expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
});
});
spies.and
By default, a spy will prevent the execution of the function which is spying, but this behaviour can be modified with .and
.and.callThrough will execute the function
.and.returnValue will return what is declared, without executing the real function: spyOn(foo, 'getBar').and.returnValue('moked result');
.and.throwError: spyOn(foo, 'setBar').and.throwError('Test error');
.and.callFake will call a mock function:
spyOn(foo, "add").and.callFake(function(firstArg, secondArg) {
return firstArg + secondArg;
});
testing angular apps
basic tests
Angular is based in classes, which makes unit testing really easy
// import the component, service, pipe, etc to test
import { LikeComponent } from './like.component';
describe('LikeComponent', () => {
// declare a variable for the element to test
let component: LikeComponent;
beforeEach(() => {
// create a new instance of the element each time
// it's easier and safer the undo everything after every test
component = new LikeComponent();
});
it('should toggle the iLike property when I click it', () => {
component.iLike = true; // setup
component.click(); // trigger
expect(component.iLike).toBe(false); // verify
// clean up is done when component is reasigned
});
// write as many test as you need
});
Testing components: the testbed
TestBed creates an isolated testing module
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { VoterComponent } from './voter.component';
describe('VoterComponent', () => {
let component: VoterComponent;
let fixture: ComponentFixture<VoterComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
// Declare the component in the mocked module
declarations: [ VoterComponent ]
});
// the fixture is a reference to the environment around the created component
// it includes the DebugElement
fixture = TestBed.createComponent(VoterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should hightliht the button if I have upvoted', () => {
component.myVote = 1;
// fixture.detectChanges fires our changes in the module
// (updates dom, fires lifecycle hooks and events, etc)
fixture.detectChanges();
let de = fixture.debugElement.query(By.css('.glyphicon-menu-up'));
expect(de.classes['highlighted']).toBeTruthy();
});
});
mocking dependencies
Never use a real dependency in a UT
import { UsersComponent } from './users.component';
import { UserService } from './user.service';
import { Observable } from 'rxjs/Observable';
describe('UsersComponent', () => {
let component: UsersComponent;
let service: UserService;
beforeEach(() => {
service = new UserService(); // real instance of the dependency
component = new UsersComponent(service); // use it to initiate the component
});
it('should set users property with the users retrieved from the server', () => {
let users = [ 1, 2, 3 ];
// but SPY every call to it
spyOn(service, 'getUsers').and.returnValue(Observable.from([ users ]));
component.ngOnInit();
expect(component.users).toBe(users);
});
});
This method can be painfull if the service would have had a lot of dependencies itself. Here is another option:
https://angular.io/guide/testing#test-a-component-with-a-dependency
Basics of unit testing
By Paqui Calabria
Basics of unit testing
- 1,262