Writing first Angular test and more
The goal of writing Automated testing is to increase the quality of software.
Angular has 2 types of Unit tests:
- Isolated test- when we are trying to check the functionality of a single function. This means it is not dependent on any other function or properties or any other things.
- integration test: Here, we combine template and logic file and test them.
Automated Testing
In automated testing, there are 3 types of testing available. These are as follows:
- Unit testing.
- E2E testing
- Integration/ functional testing Unit and E2E testing are very easy and straightforward and well defined. E2E Testing is a kind of testing against a live running app. So, testing is done with database, server and everything in action. This is done through automating Web browser. These tests are written to do thing like click buttons, type values in form, navigate to different pages and so on.
Drawbacks of E2E testing
There are some drawbacks of the E2E testing, they tend to lag in speed. also, they are difficult to write. Also, they are less reliable. Integration testing could mean one thing to someone and something to someone else.
Some diagrams to help understanding.
E2E Testing
Integration testing
Unit testing
Unit Testing
Let us talk about Unit testing. Unit testing is done against a single unit of code. Here, unit represent a single class.
Different types of Unit Test
Isolated test: Basic unit test is the isolated Test. A unit could be a business logic that needs to be tested in isolation. There are few angular units that could be tested in isolation. These are:
- Pipe
- Service
- Class
- Component
- Directives
Isolation means it should be free from dependencies. We should always mock our dependencies, else it is not isolation. In isolation testing, we don't test the template part, but only the logic which is running behind it. So, we test all the methods and for their expected behavior.
Integration Testing
An integration test is a little complex. Here, we create a module that we are going to test. There are 2 types of Integration testing. They are:
- Shallow Testing
- Deep Testing
Shallow Testing- here, we test the single component and its template Deep testing-testing both parent and child components.
Mocking Mocking is useful when we want to test a single unit of a code. We need this, because most of the time, classes does not operate in isolation. Most classes or components have dependencies. so, it will be hard to make them isolate. We could do it, using the mocking. There are 3 types of Mocks:
- Dummies- As the name suggests, it is used to create dummy classes, dependency, objects etc.
- Stubs- It will take the control/behavior. It is a class that returns a valid answer but always the same one.
- Spies- It will spy on the methods and how many times it has been called.
Running the Test
ng test
Notation of writing Unit test
// User Service has getUser() and this should return the coorect given user
describe('User Service', () => {
describe('getUser() method', () => {
it('should return the correct given user', () => {
})
})
})
Writing our first test case
Create a new file inside app folder of src with the extension .spec.test.ts as shown in the picture.
Write the below mentioned code
describe('First test',()=>{
// initialize all yje
let testVariable:any;
beforeEach(()=>{
testVariable ={};
});
it('should return true if a is true',()=>{
//first we have to arrange the data
testVariable.a=false;
//take some action. And we assume that those action will change the value of a to be true
testVariable.a=true;
//assert
expect(testVariable.a).toBe(true);
})
}
)
And, then in the console, run npm test. It will open the output which have the result obtained after running the test case.
Try the same code by changing the argument of toBe() to be false and notice the change in the output.
Writing Service for further testing
In the console, run the following command.
ng g s services/Calculator/Calculator
This will create a service file as well as a test file for the very same service.
In the service which we have created, write the below mentioned code for adding and subtracting method.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CalculatorService {
add(n1:number,n2:number){
let result=n1+n2;
return result;
}
subtract(n1:number,n2:number){
let result=n1-n2;
return result;
}
constructor() { }
}
Then, let us write the code for the testing code. the basic structure would look something like this.
describe('CalculatorService',() => {
it('should add two numbers',() => {});
it('should subtract two numbers',() => {});
})
This above code means that the test is for Calculate Service. And, then inside this we have two it() method which mentions that it should so something something. Let us see the output which comes up.
Here, we see a message called SPEC has no expectation. This is coming because we have not written what we want the expect to check and get the result and match it using some matcher function. It is okay if you did not understand this sentence, we will learn it by doing and writing more code. Basically, we have not implemented any test case inside. If we write pending() inside the anonymous function, then those it() will not be executed. Try writing it and seeing the code.
describe('CalculatorService',() => {
it('should add two numbers',() => {
pending();
});
it('should subtract two numbers',() => {
pending();
});
})
Observe the output after doing this change. Karma will understand that we have disabled the it(). It will somehow disable or keep the methods pending.
If we want to see the failed output, use fail() inside the function like shown
describe('CalculatorService',() => {
it('should add two numbers',() => {
pending();
});
it('should subtract two numbers',() => {
fail();
});
})
Now, let us start writing the test cases body.
describe('CalculatorService',() => {
it('should add two numbers',() => {
const calculator = new CalculatorService();
let result=calculator.add(1,2);
expect(result).toBe(3);
});
it('should subtract two numbers',() => {
const calculator = new CalculatorService();
let result=calculator.subtract(5,2);
expect(result).toBe(3);
});
})
After writing the code, do the necessary imports which are required. As you can see, we have created an Object of CalculatorService and then calculating the result. Then, we expect the result to be equal to the actual result which we know. Let us see the output now.
Now, for example if someone does any change in the source code, the test cases will fail. Ans, we will know that there is either some changes made in the code or in the test file. So, now we have written a simple service and tested the Service.
Testing the Service which has another Service injection through Dependency injection
Let us create a new Service named logger by running the following command
ng g s services/Logger/Logger
We will have this Logger service in order to log or the data or the result into the database or somewhere. Here is the code for the Logger Service.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggerService {
messages:string[]=[];
log(message:string){
this.messages.push(message);
}
constructor() { }
}
Here, we see that the Calculator service is dependent on Logger service. In the test file, we are creating and instance of LoggerService class and passing it as a parameter to the CalculatorService Object. Something like this
describe('CalculatorService',() => {
it('should add two numbers',() => {
const loggerService=new LoggerService();
const calculator = new CalculatorService(loggerService);
let result=calculator.add(1,2);
expect(result).toBe(3);
});
it('should subtract two numbers',() => {
const loggerService=new LoggerService();
const calculator = new CalculatorService(loggerService);
let result=calculator.subtract(5,2);
expect(result).toBe(3);
});
})
After this, we will run and see the output. I am sure that all the test cases will pass. But, here are some points which we will now need to think upon. Let's look at the code of the Logger service again.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggerService {
messages:string[]=[];
log(message:string){
this.messages.push(message);
}
constructor() { }
}
As, you can see. we are just adding the log message to an Array called messages. And, this we are doing for demo purpose. in real life scenario, a lot of services or calls like Http calls, or 3rd party API calls which could be expensive to be called or used are being used by a service. As, we could see here that we are using LoggerService called in both the it() whenever we used the CalculatorService which might be expensive in real-time. Here, Logger Service is not computing anything but it is necessary to run it. We will have situations where we are not using services or some API in the unit we are testing, but they are getting executed every time we are trying to run the unit or test the unit. Somehow, we have to do something that we call the API or the service only once or something. So, how do we deal with this?
I am thinking to keep it for another blog.
Subscribe to my newsletter
Read articles from Sunny Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by