What is Software Testing?
Ensuring that software behaves as expected and meets specified requirements.
Detect bugs early.
Ensure functionality and reliability.
Improve code maintainability.
Build confidence for deployment.
Type | Description |
---|---|
Unit Testing | Tests individual units of code in isolation. |
Integration Testing | Tests interactions between different units or modules. |
End-to-End (E2E) | Tests the entire application workflow from start to finish. |
Performance Testing | Tests the system's performance under load. |
Regression Testing | Ensures new code changes don't break existing functionality. |
UI Testing | Focuses on the user interface and user experience. |
Unit Tests (Foundation)
Most numerous and executed frequently.
Ensure individual components work as expected.
Integration Tests (Middle Layer)
Validate module interactions.
Fewer in number than unit tests.
End-to-End Tests (Top Layer)
Test complete workflows.
Few but critical for overall functionality.
Angular's Built-In Testing Tools:
Jasmine: Test framework for writing specs.
Karma: Test runner to execute tests in browsers.
Angular Testing Utilities: TestBed
, fakeAsync
, tick
, and async
.
Angular
Type | Purpose | Example |
---|---|---|
Unit Testing | Tests isolated parts of the code (e.g., services, components). | Test if a CalculatorService returns the correct sum of two numbers. |
Integration Testing | Tests how components or modules interact. | Test if a form component correctly validates user input. |
End-to-End (E2E) | Tests the application workflow as a user would experience it. | Test if the login process works end-to-end. |
Snapshot Testing | Compares the output of components or templates against a saved snapshot. | Test if a component's rendered HTML matches the expected structure. |
Focus:
Tools in Angular:
Jasmine
, Karma
, TestBed
.Characteristics:
it('should add two numbers correctly', () => {
const result = calculatorService.add(2, 3);
expect(result).toBe(5);
});
Focus:
Angular Example:
it('should pass input to child component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.componentInstance.childInput = 'Test Input';
fixture.detectChanges();
const childElement = fixture.nativeElement.querySelector('app-child');
expect(childElement.textContent).toContain('Test Input');
});
Focus:
Tool in Angular:
it('should login successfully', () => {
cy.visit('/login');
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
describe
, it
, expect
.TestBed
: Configure and initialize the testing environment.fakeAsync
and tick
: Test asynchronous operations.ComponentFixture
: Access and manipulate components.Type | Purpose |
---|---|
TestBed | Sets up the test environment for components, services, and modules. |
ComponentFixture | Allows testing and querying of component instances and DOM elements. |
fakeAsync /tick
|
Simulates asynchronous behavior for observables, promises, or timeouts. |
async | Wraps async code for execution in Angular's zone. |
Setup the Test Environment:
TestBed
to configure the testing module.Write the Test:
describe
to group tests.it
to define individual test cases.expect
to define assertions.Execute and Debug Tests:
ng test
.export class MathService {
add(a: number, b: number): number {
return a + b;
}
}
describe('MathService', () => {
let service: MathService;
beforeEach(() => {
service = new MathService();
});
it('should return the sum of two numbers', () => {
const result = service.add(2, 3);
expect(result).toBe(5);
});
});
Scenario: Test a service that adds two numbers.
@Component({
selector: 'app-message',
template: `<p>{{ message }}</p>`,
})
export class MessageComponent {
message = 'Hello, World!';
}
Scenario: Test a component that displays a message.
describe('MessageComponent', () => {
let component: MessageComponent;
let fixture: ComponentFixture<MessageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MessageComponent],
}).compileComponents();
fixture = TestBed.createComponent(MessageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should display the correct message', () => {
const compiled = fixture.nativeElement;
expect(compiled.querySelector('p').textContent).toBe('Hello, World!');
});
});
@Directive({
selector: '[appHighlight]',
})
export class HighlightDirective {
@HostBinding('style.color') color = 'blue';
}
Scenario: Test a directive that changes text color.
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
@Component({
template: `<p appHighlight>Test Text</p>`,
})
class TestComponent {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, HighlightDirective],
});
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should highlight the text with blue color', () => {
const p: HTMLElement = fixture.nativeElement.querySelector('p');
expect(p.style.color).toBe('blue');
});
});
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'reverseCapitalize',
})
export class ReverseCapitalizePipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
const reversed = value.split('').reverse().join('');
return reversed.charAt(0).toUpperCase() + reversed.slice(1);
}
}
import { ReverseCapitalizePipe } from './reverse-capitalize.pipe';
describe('ReverseCapitalizePipe', () => {
let pipe: ReverseCapitalizePipe;
beforeEach(() => {
pipe = new ReverseCapitalizePipe();
});
it('should reverse a string and capitalize the first letter', () => {
const result = pipe.transform('angular');
expect(result).toBe('Ralugna');
});
it('should handle empty strings', () => {
const result = pipe.transform('');
expect(result).toBe('');
});
});
import { ReverseCapitalizePipe } from './reverse-capitalize.pipe';
describe('ReverseCapitalizePipe', () => {
let pipe: ReverseCapitalizePipe;
beforeEach(() => {
pipe = new ReverseCapitalizePipe();
});
it('should handle undefined input gracefully', () => {
const result = pipe.transform(undefined as any);
expect(result).toBe('');
});
it('should not alter single-character strings other than capitalizing them', () => {
const result = pipe.transform('a');
expect(result).toBe('A');
});
it('should reverse and capitalize strings with spaces correctly', () => {
const result = pipe.transform('hello world');
expect(result).toBe('Dlrow olleh');
});
});
Scenario: Test a pipe that changes text to reverse capitalized text
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class DataService {
constructor(private http: HttpClient) {}
fetchData(): Observable<any> {
return this.http.get('https://api.example.com/data');
}
}
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService],
});
service = TestBed.inject(DataService);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify(); // Ensures no pending HTTP requests are left.
});
// tests here
});
it('should fetch data from the API', () => {
const mockData = { id: 1, name: 'John Doe' };
service.fetchData().subscribe((data) => {
expect(data).toEqual(mockData);
});
const req = httpTestingController.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('GET');
// Simulate a successful HTTP response with mock data.
req.flush(mockData);
});
it('should handle HTTP errors gracefully', () => {
const mockError = { status: 404, statusText: 'Not Found' };
service.fetchData().subscribe({
next: () => fail('Expected an error, not data'),
error: (error) => {
expect(error.status).toBe(404);
expect(error.statusText).toBe('Not Found');
},
});
const req = httpTestingController.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('GET');
// Simulate an HTTP error response.
req.flush(null, mockError);
});
Scenario: Test an http request with httpClient observables.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-signal-example',
template: `<p>{{ count() }}</p>`,
})
export class SignalExampleComponent {
count = signal(0);
increment() {
this.count.set(this.count() + 1);
}
decrement() {
this.count.set(this.count() - 1);
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SignalExampleComponent } from './signal-example.component';
describe('SignalExampleComponent', () => {
let component: SignalExampleComponent;
let fixture: ComponentFixture<SignalExampleComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SignalExampleComponent],
}).compileComponents();
fixture = TestBed.createComponent(SignalExampleComponent);
component = fixture.componentInstance;
});
it('should initialize count signal to 0', () => {
expect(component.count()).toBe(0);
});
// more tests
});
it('should increment count signal', () => {
component.increment();
expect(component.count()).toBe(1);
});
it('should decrement count signal', () => {
component.increment();
component.increment();
component.decrement();
expect(component.count()).toBe(1);
});
Scenario: Test a counter state that is registered with signal.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-computed-example',
template: `<p>{{ doubleCount() }}</p>`,
})
export class ComputedExampleComponent {
count = signal(5);
doubleCount = computed(() => this.count() * 2);
increment() {
this.count.set(this.count() + 1);
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComputedExampleComponent } from './computed-example.component';
describe('ComputedExampleComponent', () => {
let component: ComputedExampleComponent;
let fixture: ComponentFixture<ComputedExampleComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ComputedExampleComponent],
}).compileComponents();
fixture = TestBed.createComponent(ComputedExampleComponent);
component = fixture.componentInstance;
});
// tests
});
it('should initialize doubleCount computed signal correctly', () => {
expect(component.doubleCount()).toBe(10);
});
it('should update doubleCount when count changes', () => {
component.increment();
expect(component.doubleCount()).toBe(12);
});
Scenario: Test a counter state that double is registered with computed.
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-effect-example',
template: `<p>Log Count: {{ count() }}</p>`,
})
export class EffectExampleComponent {
count = signal(0);
constructor() {
effect(() => {
console.log(`Count has changed: ${this.count()}`);
});
}
increment() {
this.count.set(this.count() + 1);
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EffectExampleComponent } from './effect-example.component';
describe('EffectExampleComponent', () => {
let component: EffectExampleComponent;
let fixture: ComponentFixture<EffectExampleComponent>;
let consoleSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [EffectExampleComponent],
}).compileComponents();
fixture = TestBed.createComponent(EffectExampleComponent);
component = fixture.componentInstance;
consoleSpy = spyOn(console, 'log');
});
// tests
});
it('should log message when count changes', () => {
component.increment();
expect(consoleSpy).toHaveBeenCalledWith('Count has changed: 1');
});
it('should log the correct message after multiple increments', () => {
component.increment();
component.increment();
component.increment();
expect(consoleSpy.calls.mostRecent().args[0]).toBe('Count has changed: 3');
});
Scenario: Test a counter state that call effect on change of state with effect.
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class MyService {
fetchData(): Observable<string[]> {
// Simulating a data fetch
return of(['Item1', 'Item2']);
}
}
Scenario: Test a fetching service integration with a component
import { Component, OnInit } from '@angular/core';
import { MyService } from './my.service';
@Component({
selector: 'app-my-component',
template: `
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
`,
})
export class MyComponent implements OnInit {
items: string[] = [];
constructor(private myService: MyService) {}
ngOnInit(): void {
this.myService.fetchData().subscribe((data) => {
this.items = data;
});
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MyComponent } from './my.component';
import { MyService } from './my.service';
describe('MyComponent Integration Test', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [{ provide: MyService, useClass: MockMyService }],
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// tests
});
Scenario: Test a fetching service integration with a component
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should fetch data and display it in the template', () => {
// Verify the items are set correctly
expect(component.items).toEqual(['MockItem1', 'MockItem2']);
// Verify the template renders the items
const items = fixture.debugElement.queryAll(By.css('li'));
expect(items.length).toBe(2);
expect(items[0].nativeElement.textContent).toContain('MockItem1');
expect(items[1].nativeElement.textContent).toContain('MockItem2');
});
npx cypress open
cypress/fixtures/
: Sample data for tests.cypress/integration/
: Your test files.cypress/support/
: Utility files for shared code.npm install cypress --save-dev
touch cypress/integration/homepage.spec.js
Create a file in cypress/integration/
:
describe('Homepage Tests', () => {
it('should load the homepage', () => {
cy.visit('/');
cy.contains('Welcome to My App').should('be.visible');
});
});
Write the test:
touch cypress/integration/form-page.spec.js
Create a file in cypress/integration/
:
describe('Form Tests', () => {
it('should fill and submit the form', () => {
cy.visit('/form-page');
cy.get('#name').type('John Doe');
cy.get('#email').type('john.doe@example.com');
cy.get('button[type="submit"]').click();
cy.contains('Form submitted successfully!').should('be.visible');
});
});
Write the test:
touch cypress/integration/navigation-page.spec.js
Create a file in cypress/integration/
:
describe('Navigation Tests', () => {
it('should navigate to About page', () => {
cy.visit('/');
cy.get('a[href="/about"]').click();
cy.url().should('include', '/about');
cy.contains('About Us').should('be.visible');
});
});
Write the test:
touch cypress/integration/api.spec.js
Create a file in cypress/integration/
:
describe('API Tests', () => {
it('should display mocked data', () => {
cy.intercept('GET', '/api/items', {
statusCode: 200,
body: [{ id: 1, name: 'Mock Item' }],
}).as('getItems');
cy.visit('/items');
cy.wait('@getItems');
cy.contains('Mock Item').should('be.visible');
});
});
Write the test:
touch cypress/integration/responsive.spec.js
Create a file in cypress/integration/
:
describe('Responsive Design Tests', () => {
it('should work on mobile', () => {
cy.viewport('iphone-6');
cy.visit('/');
cy.contains('Mobile View').should('be.visible');
});
});
Write the test:
touch cypress/support/commands.js
Create a file in cypress/integration/
:
Cypress.Commands.add('login', (username, password) => {
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('button[type="submit"]').click();
});
Write the command:
cy.login('user1', 'password123');
using it in specs
Let's practice