Teniendo la siguiente estructura de carpetas y archivos
my-app/
├─ src/
│ ├─ setup-jest.ts
│ ├─ app/
│ ├─ test/
│ │ ├─ index.d.ts
│ │ ├─ __mocks__/
│ │ │ ├─ jest-global-mock.ts
│ │ ├─ __stubs__/
│ │ │ ├─ ActivatedRouteStub.ts
│ │ │ ├─ MatDialogStub.ts
├─ jest.config.ts
├─ angular.json
├─ tsconfig.spec.json
├─ tsconfig.json
1.Instalar Jest, Angular Testing Library y Translate testing
npm install --save-dev @angular-builders/jest @testing-library/angular @testing-library/jest-dom @testing-library/user-event jest@28 jest-preset-angular ts-jest ts-node @types/jest ngx-translate-testing jest-environment-jsdom
2.Remover paquetes innecesarios
npm remove @types/jasmine @types/jasminewd2 jasmine-core jasmine-spec-reporter karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine karma-jasmine-html-reporter karma-coverage protractor
3.Ejecutar lo siguiente
rm src/test.ts src/karma.conf.js
touch jest.config.ts src/setup-jest.ts
mkdir -p src/test/__mocks__ src/test/__stubs__
touch src/test/index.d.ts src/test/__mocks__/jest-global-mocks.ts src/test/__stubs__/ActivatedRouteStub.ts src/test/__stubs__/MatDialogStub.ts
4.En tsconfig.json
, agregar lo siguiente
{
compilerOptions: {
"paths": {
"@app/*": ["src/app/*"],
"@test/*": ["src/app/test/*"]
}
}
}
5.Crear archivo jest.config.ts
en la raiz y agregar lo siguiente:
import type { Config } from '@jest/types'
import { pathsToModuleNameMapper } from 'ts-jest'
import { compilerOptions } from './tsconfig.json'
const config: Config.InitialOptions = {
preset: 'jest-preset-angular',
globalSetup: 'jest-preset-angular/global-setup',
modulePathIgnorePatterns: ['<rootDir>/dist/'],
moduleNameMapper: {
'src/(.*)': '<rootDir>/src/$1',
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>' }),
},
transformIgnorePatterns: ['/node_modules/(?!.*\\.mjs$|@braze)'],
setupFilesAfterEnv: ['<rootDir>/src/setup-jest.ts'],
}
export default config
6.Configurar archivo angular.json
...
"test": {
"builder": "@angular-builders/jest:run",
"options": {
"no-cache": true
}
},
...
7.Editar y mover archivo tsconfig.spec.json
a la raiz
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"types": ["jest", "node"],
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"noImplicitAny": false
},
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}
8.Crear archivo src/setup-jest.ts
import 'jest-preset-angular/setup-jest'
import '@testing-library/jest-dom'
import './test/__mocks__/jest-global-mocks'
import { configure } from '@testing-library/angular'
import { TranslateTestingModule } from 'ngx-translate-testing'
import * as defaultTranslation from './assets/i18n/en.json'
import { LocaleModule } from '@app/locale/locale.module'
configure({
defaultImports: [
TranslateTestingModule.withTranslations('en', defaultTranslation),
LocaleModule,
],
dom: {
testIdAttribute: 'id',
},
})
9.Crear archivo src/test/__mocks__/jest-global-mocks.ts
const createLocalStorageMock = (): {
getItem(key: string): any | null
setItem(key: string, value: any): void
clear(): void
} => {
let storage: {
[prop: string]: any
} = {}
return {
getItem: (key) => (storage[key] ? storage[key] : null),
setItem: (key, value) => (storage[key] = value),
clear: (): void => {
storage = {}
},
}
}
Object.defineProperty(window, 'localStorage', {
value: createLocalStorageMock(),
})
HTMLAnchorElement.prototype.click = jest.fn()
10.En package.json
, modificar lo siguiente
{
...
"scripts": {
...
"test": "jest --no-cache --onlyChanged",
"test:handle": "jest --no-cache --detectOpenHandles --forceExit --onlyChanged",
"test:all": "jest --no-cache",
"test:coverage": "jest --no-cache --coverage",
...
},
...
}
Explicación:
npm run test
: Ejecuta los test que solo se han modificadonpm run test:handle
: Ejecuta los test que solo se han modificado y contienen código aíncrono por RxJsnpm run test:all
: Ejecuta todos los test de la aplicaciónnpm run test:coverage
: Ejecuta el coverage de la aplicación
11.Crear archivo src/test/index.d.ts
interface ClipboardItem {
readonly types: ReadonlyArray<string>
getType(type: string): Promise<Blob>
}
Carpeta Stubs
Archivo src/test/__stubs__/ActivatedRouteStub.ts
Se utiliza esta clase cuando queremos simular los queryParams
o datos que se encuentran en la URL.
/* eslint-disable @typescript-eslint/member-ordering */
import { convertToParamMap, ParamMap, Params } from '@angular/router'
import { ReplaySubject } from 'rxjs'
export class ActivatedRouteStub {
_params: Params
_queryParam: Params
private subjectParams = new ReplaySubject<Params>()
private subjectParamMap = new ReplaySubject<ParamMap>()
private subjectQueryParams = new ReplaySubject<ParamMap>()
readonly params = this.subjectParams.asObservable()
readonly paramMap = this.subjectParamMap.asObservable()
readonly queryParamMap = this.subjectQueryParams.asObservable()
constructor({ initialParams, initialQueryParams }: { initialParams?: Params; initialQueryParams?: Params } = {}) {
this._params = initialParams || {}
this._queryParam = initialQueryParams || {}
this.setParams(initialParams)
this.setQueryParams(initialQueryParams)
}
get snapshot(): {
params: Params
queryParams: Params
paramMap: ParamMap
} {
return {
params: this._params,
queryParams: this._queryParam,
paramMap: convertToParamMap(this._params),
}
}
setParams(params: Params = {}): void {
this.subjectParams.next(params)
this.subjectParamMap.next(convertToParamMap(params))
}
setQueryParams(params: Params = {}): void {
this.subjectQueryParams.next(convertToParamMap(params))
}
}
Archivo src/test/stubs/MatDialogStub.ts
Se utiliza esta clase cuando queremos testear componentes administrados por el matDialog
import { Observable, of } from 'rxjs'
export class MatDialogStub {
_result: boolean
constructor(result = true) {
this._result = result
}
setResult(val: boolean): void {
this._result = val
}
open(): { afterClosed: () => Observable<boolean> } {
return { afterClosed: () => of(this._result) }
}
}
Base de una prueba a un componente
Configuración mínima:
import userEvent from '@testing-library/user-event'
import { fireEvent, render, screen } from '@testing-library/angular'
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { ButtonComponent } from './button.component'
import { fakeAsync, flush } from '@angular/core/testing'
async function setupComponent() {
return await render(ButtonComponent, {
declarations: [],
imports: [],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
}
describe('ButtonComponent', () => {
beforeEach(() => {
jest.restoreAllMocks()
})
it('Should create component correctly', async () => {
const { fixture } = await setupComponent()
expect(fixture.componentInstance).toBeTruthy()
})
it('When I ...', async () => {
await setupComponent()
})
})
Configuración extendida:
import userEvent from '@testing-library/user-event'
import { fireEvent, render, screen } from '@testing-library/angular'
import { fakeAsync, flush } from '@angular/core/testing'
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { SnackbarNotifierComponent } from '@app/shared/components/notifier/snackbar_notifier.component'
import { NOTIFIER_TOKEN } from '@app/shared/components/notifier/notifier.interface'
import { BottlersListComponent } from './bottlers-list.component'
import { BottlersService } from '../bottlers.service'
import { BottlersServiceMock } from '@app/test/__mocks__/bottler/bottler.service.mock'
import { MatSnackBarModule } from '@angular/material/snack-bar'
async function setupComponent() {
return await render(BottlersListComponent, {
declarations: [],
imports: [MatSnackBarModule],
providers: [
{ provide: NOTIFIER_TOKEN, useClass: SnackbarNotifierComponent },
{ provide: BottlersService, useValue: new BottlersServiceMock() },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
}
describe('BottlersListComponent', () => {
beforeEach(() => {
jest.restoreAllMocks()
})
it('Should create component correctly', async () => {
const { fixture } = await setupComponent()
expect(fixture.componentInstance).toBeTruthy()
})
it('When I ...', fakeAsync(async () => {
await setupComponent()
flush()
}))
})
Top comments (0)