Angular Testing Library - Instalación en Angular
Published: 2023-01-23 20:54:58

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
```javascript
{
  compilerOptions: {
    "paths": {
      "@app/*": ["src/app/*"],
      "@test/*": ["src/app/test/*"]
    }
  }
}
```

5.Crear archivo `jest.config.ts` en la raiz y agregar lo siguiente:
```javascript
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`
```javascript
...
"test": {
  "builder": "@angular-builders/jest:run",
  "options": {
    "no-cache": true
  }
},
...
```

7.Editar y mover archivo `tsconfig.spec.json` a la raiz

```javascript
{
  "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`
```javascript
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`
```javascript
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
```javascript
{
  ...
  "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:**
> 1. `npm run test`: Ejecuta los test que solo se han modificado
> 2. `npm run test:handle`: Ejecuta los test que solo se han modificado y contienen código aíncrono por RxJs
> 3. `npm run test:all`: Ejecuta todos los test de la aplicación
> 4. `npm run test:coverage`: Ejecuta el coverage de la aplicación

11.Crear archivo `src/test/index.d.ts`
```javascript
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.

```javascript
/* 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`

```javascript
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:

```javascript
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:

```javascript
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()
  }))
})
```