10 KiB
10 KiB
name, description
| name | description |
|---|---|
| testing-deployment-implementation | Write unit tests for components and services, implement E2E tests with Cypress, set up test mocks, optimize production builds, configure CI/CD pipelines, and deploy to production platforms. |
Testing & Deployment Implementation Skill
Unit Testing Basics
TestBed Setup
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
});
Component Testing
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserListComponent],
imports: [CommonModule, HttpClientTestingModule],
providers: [UserService]
}).compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('should display users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
component.users = mockUsers;
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const userElements = compiled.querySelectorAll('.user-item');
expect(userElements.length).toBe(2);
});
it('should call service on init', () => {
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(of([]));
component.ngOnInit();
expect(userService.getUsers).toHaveBeenCalled();
});
});
Testing Async Operations
// Using fakeAsync and tick
it('should load users after delay', fakeAsync(() => {
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(
of([{ id: 1, name: 'John' }]).pipe(delay(1000))
);
component.ngOnInit();
expect(component.users.length).toBe(0);
tick(1000);
expect(component.users.length).toBe(1);
}));
// Using waitForAsync
it('should handle async operations', waitForAsync(() => {
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(
of([{ id: 1, name: 'John' }])
);
component.ngOnInit();
fixture.whenStable().then(() => {
expect(component.users.length).toBe(1);
});
}));
Mocking Services
HTTP Mocking
it('should fetch users from API', () => {
const mockUsers: User[] = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users.length).toBe(1);
expect(users[0].name).toBe('John');
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
// POST with error handling
it('should handle errors', () => {
service.createUser({ name: 'Jane' }).subscribe(
() => fail('should not succeed'),
(error) => expect(error.status).toBe(400)
);
const req = httpMock.expectOne('/api/users');
req.flush('Invalid user', { status: 400, statusText: 'Bad Request' });
});
Service Mocking
class MockUserService {
getUsers() {
return of([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
}
}
@Component({
selector: 'app-test',
template: '<div>{{ (users$ | async)?.length }}</div>'
})
class TestComponent {
users$ = this.userService.getUsers();
constructor(private userService: UserService) {}
}
describe('TestComponent with Mock', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [
{ provide: UserService, useClass: MockUserService }
]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should render users', () => {
const div = fixture.nativeElement.querySelector('div');
expect(div.textContent).toContain('2');
});
});
E2E Testing with Cypress
Basic E2E Test
describe('User List Page', () => {
beforeEach(() => {
cy.visit('/users');
});
it('should display user list', () => {
cy.get('[data-testid="user-item"]')
.should('have.length', 10);
});
it('should filter users by name', () => {
cy.get('[data-testid="search-input"]')
.type('John');
cy.get('[data-testid="user-item"]')
.should('have.length', 1)
.should('contain', 'John');
});
it('should navigate to user detail', () => {
cy.get('[data-testid="user-item"]').first().click();
cy.location('pathname').should('include', '/users/');
cy.get('[data-testid="user-detail"]').should('be.visible');
});
});
Page Object Model
// user.po.ts
export class UserPage {
navigateTo(path: string = '/users') {
cy.visit(path);
return this;
}
getUsers() {
return cy.get('[data-testid="user-item"]');
}
getUserByName(name: string) {
return cy.get('[data-testid="user-item"]').contains(name);
}
clickUser(index: number) {
this.getUsers().eq(index).click();
return this;
}
searchUser(query: string) {
cy.get('[data-testid="search-input"]').type(query);
return this;
}
}
// Test using PO
describe('User Page', () => {
const page = new UserPage();
beforeEach(() => {
page.navigateTo();
});
it('should find user by name', () => {
page.searchUser('John');
page.getUsers().should('have.length', 1);
});
});
Build Optimization
AOT Compilation
// angular.json
{
"projects": {
"app": {
"architect": {
"build": {
"options": {
"aot": true,
"outputHashing": "all",
"sourceMap": false,
"optimization": true,
"buildOptimizer": true,
"namedChunks": false
}
}
}
}
}
}
Bundle Analysis
# Install webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# Run analysis
ng build --stats-json
webpack-bundle-analyzer dist/app/stats.json
Code Splitting
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'users',
loadChildren: () =>
import('./users/users.module').then(m => m.UsersModule)
}
];
Deployment
Production Build
# Build for production
ng build --configuration production
# Output directory
dist/app/
# Serve locally
npx http-server dist/app/
Deployment Targets
Firebase:
npm install -g firebase-tools
firebase login
firebase init hosting
firebase deploy
Netlify:
npm run build
# Drag and drop dist/ folder to Netlify
# Or use CLI:
npm install -g netlify-cli
netlify deploy --prod --dir=dist/app
GitHub Pages:
ng build --output-path docs --base-href /repo-name/
git add docs/
git commit -m "Deploy to GitHub Pages"
git push
# Enable in repository settings
Docker:
# Build stage
FROM node:18 as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Serve stage
FROM nginx:alpine
COPY --from=build /app/dist/app /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
CI/CD Pipelines
GitHub Actions
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm run test -- --watch=false --code-coverage
- name: E2E Test
run: npm run e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
- name: Deploy
if: github.ref == 'refs/heads/main'
run: npm run deploy
Performance Monitoring
Core Web Vitals
// Using web-vitals library
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
Error Tracking (Sentry)
import * as Sentry from "@sentry/angular";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
integrations: [
new Sentry.BrowserTracing(),
new Sentry.Replay(),
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
@NgModule({
providers: [
{
provide: ErrorHandler,
useValue: Sentry.createErrorHandler(),
},
],
})
export class AppModule {}
Testing Best Practices
- Arrange-Act-Assert: Clear test structure
- One Assertion per Test: Keep tests focused
- Test Behavior: Not implementation details
- Use Page Objects: For E2E tests
- Mock External Dependencies: Services, HTTP
- Test Error Cases: Invalid input, failures
- Aim for 80% Coverage: Don't obsess over 100%
Coverage Report
# Generate coverage report
ng test --code-coverage
# View report
open coverage/index.html