unit Testing in Angular services
Master Unit Testing in Angular: A Beginner's Guide to Jasmine and Karma
Introduction: Why Unit Testing is Something You Can't Ignore
- What are Jasmine and Karma?
- How to use them properly to test both components and services.
- Real examples (Yes, we will!) such as reading from an external data source or engaging in user interactions.
- Helpful hints for beginners and advanced users alike.
By the end, you gain the ability to write tests that handle adversity while bringing pleasure and satisfaction every time they pass!
Jasmine & Karma: Descriptions & Connections
Jasmine: The Testing Framework
Jasmine is a behavior-driven development (BDD) framework for human-readable test writing. It uses natural language, making it easy to write and read tests.
Characteristic Features:
describe()
: Groups related tests (test suites).it()
: Defines individual test cases.expect()
: Asserts that something will happen.
Karma: The Test Runner
Karma runs your tests in real browsers (Chrome, Firefox) or various "headless" environments. Moreover, Karma re-runs tests after changes in source code.
Why Join Them Together?
- Jasmine writes the tests.
- Karma executes those tests across different browsers.
- Both integrate seamlessly with Angular's CLI.
How to Set Jasmine & Karma Up within Angular
1. Angular CLI: It's All Built In
When starting a new Angular project, Jasmine and Karma come pre-configured. Simply check the files karma.conf.js
and src/test.ts
to see how things are set up.
2. Running Your First Test
ng test
This command launches Karma, executes all tests, and displays a live-running browser with outputs.
Writing Your First Test
Example: Testing a Simple Service
We'll test a CalculatorService
with an add()
method.
Step 1: Create the Service
// calculator.service.ts
@Injectable({
providedIn: 'root'
})
export class CalculatorService {
add(a: number, b: number): number {
return a + b;
}
}
Step 2: Write the Test
// calculator.service.spec.ts
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should add two numbers', () => {
expect(service.add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(service.add(-1, 5)).toBe(4);
});
});
Step 3: Run the Test
ng test
Karma reports back with either green checks (for passing tests) or red Xs (for failures).
Real Test Scenarios
1. Components with User Interaction
This test checks if a LoginComponent
emits its login
event when the button is clicked.
// login.component.spec.ts
it('should emit login event on button click', () => {
spyOn(component.login, 'emit');
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(component.login.emit).toHaveBeenCalled();
});
2. Mocking HTTP Requests
Testing a DataService
that fetches data from an API using HttpClientTestingModule
.
// data.service.spec.ts
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('DataService', () => {
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule]
});
httpMock = TestBed.inject(HttpTestingController);
});
it('should fetch users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
req.flush(mockUsers);
httpMock.verify();
});
});
Best Practices for Successful Testing
- Make each
it()
focused: Tests should be concise and targeted. - Use Descriptive Names:
it('should print error when email is invalid')
>it('testing form')
. - Mock Dependencies: Use spies or stubs to isolate tests.
- Prioritize Critical Paths: Focus on areas with the highest user impact.
- Always Run Tests: Integrate tests into CI/CD pipelines (e.g., GitHub Actions).