Написание тестов аналогично дегустации еды перед ее подачей. Важность модульного тестирования известна на всех уровнях программирования, но чаще всего игнорируется разработчиками пользовательского интерфейса. Этот пост представляет собой краткое описание того, как вы можете начать свой путь к тому, чтобы стать лучшим фронтенд-инженером, включив в свой код эти ключевые концепции модульного тестирования.

Обзор

1.Важность модульного тестирования

2. образец приложения

  • Настраивать
  • Написание модульных тестов для презентационных компонентов
  • Написание модульных тестов для сервисов
  • Написание модульных тестов для компонентов контейнера

3. Заключение

Важность модульного тестирования

Написание модульных тестов действительно похоже на накладные расходы, когда вы можете просто протестировать функциональность, используя его. Если вы столкнетесь с подобной дилеммой, помните об этих нескольких моментах:

  1. Модульные тесты не только улучшают качество, но и сокращают время отладки. Модульные тесты помогают понять, какие части приложения работают должным образом, а какие нет, и, следовательно, позволяют сузить круг причин ошибки гораздо быстрее, чем при использовании console.logs или отладчиков.
  2. Мы разработчики JS !!: все мы, как разработчики, либо создали тестовые компоненты пользовательского интерфейса и грубый HTML-код для тестирования базовой логики / службы, либо отложили тестирование до тех пор, пока наши презентационные компоненты не будут готовы. Написание модульного теста позволяет итеративно создавать функциональный компонент без лишних элементов тестового пользовательского интерфейса.
  3. Свобода сотрудничества. Работая в команде, я часто замечал, что участники работают над изолированными функциональными возможностями, а с большой базой кода постоянно возникает страх сломать какой-то рабочий код во время рефакторинга и исправления ошибок. Этого следует и можно избежать, если вы напишете правильные модульные тесты вместе с кодом, который обнаруживает любые поломки в случае изменений для разработчиков, которые могут работать над кодом позже.
  4. Отсутствие низкоуровневой документации модульный тест декларирует цель данной единицы кода. Это снижает потребность разработчика в явном документировании кода (также рекомендуется декларативный стиль программирования для всех разработчиков JS), а продуктовые группы могут больше сосредоточиться на внешнем виде приложения, чем на функциональности.
    Использование тестовых фреймворков. Подобно Jest, вы также можете тестировать код Frontend в ваших средах CI / CD, что является плюсом до настоящего момента. 3, поскольку он помогает генерировать регулярные отчеты о состоянии вашего кода и охвате тестами.

Вот несколько основных рекомендаций, которые следует учитывать при написании модульных тестов:

  1. Понимание типа модульных тестов, которые следует написать, зависит от типа компонента приложения (презентационный, логические контейнеры, службы и т. Д.). Понимание того, что следует тестировать, действительно помогает обосновать дополнительные усилия, которые вы прилагаете при написании модульных тестов на каждом уровне.
  2. Напишите функциональный JS и постарайтесь как можно больше разбить свое приложение на презентационные и логические компоненты. Это действительно помогает улучшить фокус ваших модульных тестов, а также сокращает время, затрачиваемое на их написание.
  3. Пишите тесты вместе с кодом. Это, безусловно, самый важный !! Я не могу не подчеркнуть, насколько болезненным было для меня пересмотр старого кода и добавление модульных тестов для уже разработанных компонентов. Требуются время и усилия, чтобы понять, что вы написали и что тестировать. Когда тесты пишутся, нашей целью должно быть написание кода, который проходит тесты, а не наоборот.
  4. Попрактикуйтесь в написании тестов, прежде чем погрузиться в написание своего приложения. Большинство разработчиков избегают написания тестов, потому что они либо не знают, либо не совсем уверены в некоторых основах, таких как «Мокинг класса», тестирование асинхронного вызова, имитация HTTP-звонков и т. Д. Избавьтесь от этих заблуждений и мифов с практикой. Так что практикуйте модульное тестирование так же, как и пишите код приложения.

Поняв важность написания тестов, мы рассмотрим пример приложения Angular и напишем для него несколько модульных тестов с помощью Jest.

Почему шутка?

Jest - это красивая среда тестирования, которая обеспечивает единообразные варианты модульного тестирования, не основанные на браузере, для нескольких платформ javascript.

Узнайте о них больше здесь.

Также приветствуем библиотеку jest-angular-preset, которая упрощает использование jest с angular. С помощью jest я получаю три замечательные функции, которых нет при стандартной настройке тестирования angular: тестирование моментальных снимков, модульные тесты, которые могут выполняться без браузера, и AutoMocking. Я предлагаю всем понять это, чтобы использовать этот замечательный фреймворк в полной мере.

Настраивать :

Если вы никогда раньше не использовали angular, пожалуйста, следуйте официальному руководству по установке angular здесь

Наше приложение будет иметь три основных компонента: AppComponent, ListingService, ListRowComponent. Но прежде чем мы начнем писать наши компоненты и тестовые примеры, мы должны настроить jest.

Шаги по настройке шутки:

Используйте это краткое руководство, чтобы выполнить первоначальную настройку, удалить код, основанный на карме, и запустить jest.

Jest позволяет хранить вашу конфигурацию либо в поле шутки в вашем package.json, либо в отдельном файле jest.config.js

Я бы посоветовал всем один раз ознакомиться с официальным руководством по настройке, чтобы узнать, какие конфигурации могут быть у вашего проекта и которые могут потребоваться. Чтобы помочь вам, я бы рекомендовал хотя бы сосредоточиться на следующих областях: setupFilesAfterEnv, coverageDirectory, coverageReporters, transformIgnorePatterns, modulePathIgnorePatterns, moduleNameMapper, testPathIgnorePatterns

Вот jest.config.js из нашего примера приложения

module.exports = {
    "preset": "jest-preset-angular",
    "setupFilesAfterEnv": ["<rootDir>/setupJest.ts"],
    globals: {
      "ts-jest": {
        tsConfig: '<rootDir>/tsconfig.spec.json',
        "diagnostics":false,
        "allowSyntheticDefaultImports": true,
        "stringifyContentPathRegex": "\\.html$",
        astTransformers: [require.resolve('jest-preset-angular/InlineHtmlStripStylesTransformer')],
      }
    },
    coverageDirectory:'<rootDir>/output/coverage/jest',
    transformIgnorePatterns: ["node_modules/"],
    "coverageReporters": [
      "text",
      "json",
    ],
    "reporters": [
      "default",
    ],
    snapshotSerializers: [
      'jest-preset-angular/AngularSnapshotSerializer.js',
      "jest-preset-angular/AngularSnapshotSerializer.js",
      "jest-preset-angular/HTMLCommentSerializer.js"
    ],
    "transform": {
      '^.+\\.(ts|html)$': 'ts-jest',
      "^.+\\.js$": "babel-jest",
    },
    modulePathIgnorePatterns: [],
    moduleNameMapper: {},
    testPathIgnorePatterns:['sampleCodes/'],
  };

Вот мой tsconfig.spec.ts

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["jest", "node"],
    "emitDecoratorMetadata": true,
    "allowJs": true
  },
  "files": [
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.spec.ts",
    "src/**/*.d.ts"
  ]
}

Примечание. Не копируйте и вставляйте код просто, но понимание конфигурации действительно поможет вам настроить всю конфигурацию для вашего проекта самостоятельно.

Я бы также предложил установить jest глобально

npm install -g jest

Это действительно помогает при запуске команд jest cli, необходимых для тестирования снимков (например, при обновлении снимков с помощью jest -u).

Наконец, запустите jest и проверьте, выполняются ли базовые тесты, автоматически созданные с помощью ng generate, с использованием

jest --coverage

Вот отличное руководство о том, как тестировать компоненты и улучшать наши тестовые примеры, и как библиотека DOM Testing помогает в этом

Написание модульных тестов для презентационных компонентов

Если вы на практике пишете чистые презентационные компоненты, тогда вы молодец !!. Если нет, я предлагаю вам начать практиковаться в том, как разделить код вашего приложения на логические контейнеры и презентационные компоненты.

Jest имеет возможность использовать тестирование моментальных снимков для тестирования компонентов пользовательского интерфейса. Подробнее о тестировании снимков здесь

Это экономит время, затрачиваемое на написание запросов DOM. Согласно документации, эти снимки следует фиксировать вместе с кодом, чтобы вы могли проверить, как компоненты пользовательского интерфейса должны отображаться в DOM.

Когда не использовать снимки?

Если компонент является базовым и достаточно простым, тестирование снимков должно охватывать большинство ваших тестов пользовательского интерфейса, хотя избегайте его использования с презентационными компонентами, такими как списки, где вы хотите проверить общее количество отображаемых строк или в компонентах, где проверка представления бизнес-логики требуется.

Ниже найти образец ListRowComponent

@Component({
  selector: 'app-list-row-component',
  templateUrl: './list-row-component.component.html',
  styleUrls: ['./list-row-component.component.scss'],
})
export class ListRowComponentComponent implements OnInit {
  @Input() firstName:string;
  @Input() lastName:string;
  @Input() gender:string;
  @Output() rowClick = new EventEmitter();
  getClass(){
    return {
      'blue':this.gender==='male',
      'green':this.gender==='female'
    }
  }
  constructor() { 
  }
  ngOnInit() {
  }
}

Ниже найдите образец файла ListRowComponent.spec

describe('ListRowComponentComponent', () => {
  let component: ListRowComponentComponent;
  let fixture: ComponentFixture<ListRowComponentComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ListRowComponentComponent ]
    })
    .compileComponents();
  }));
  beforeEach(() => {
    fixture = TestBed.createComponent(ListRowComponentComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  it('should render the component with blue color class',()=>{
    component.firstName = 'James'
    component.lastName = 'Bond'
    component.gender = 'male'
    fixture.detectChanges()
    expect(fixture).toMatchSnapshot();
  })
  it('should render the component with green color class',()=>{
    component.firstName = 'James'
    component.lastName = 'Bond'
    component.gender = 'female'
    fixture.detectChanges()
    expect(fixture).toMatchSnapshot();
  })
  it('should emit events onClick',done=>{
    let buttonClicked = false
    component.rowClick.subscribe(()=>{
      buttonClicked =true;
      expect(buttonClicked).toBeTruthy()
      done();
    })
    var btn = getByTestId(fixture.nativeElement,'row-click');
    simulateClick(btn);
  })
});

Примечание. Если вы заметили, я использую data-testid для запроса кнопки в модульном тесте выше. Я бы посоветовал всем разработчикам применить это на практике. Это делает наши тесты очень устойчивыми к изменениям и надежными по своей природе.

Написание модульных тестов для сервисов

Во-первых, вот несколько концепций, которые меня сбивали с толку до того, как я начал писать модульные тесты для сервисов или контейнеров:

Мокинг зависимостей. Есть много отличных руководств, доступных с помощью простого поиска в Google по этому вопросу, но большинство из них используют конструкторы компонентов или продвигают с помощью функций автоматического мокинга Jest for Mocking dependencies. Это зависит от ваших предпочтений, какой метод вы используете. Для меня ключевым моментом было высмеивание зависимостей при использовании Angular Dependency Injection для создания экземпляра компонента, и я нашел действительно хороший способ сделать это.

Вы можете прочитать эту замечательную статью о том же

Mocking Store: рекомендуется написать геттеры и селекторы для хранилища ngrx (https://ngrx.io/) в службах, чтобы ваши компоненты можно было повторно использовать вместе с хранилищем. Это означает, что очень важно высмеивать действующий магазин.

describe('Service:StoreService', () => {
  let backend: HttpTestingController;
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule, HttpClientTestingModule, RouterTestingModule],
      providers: [
        provideMockStore({ initialState }),
      ],
      schemas:[NO_ERRORS_SCHEMA]
    });
    backend = TestBed.get(HttpTestingController);
  });
  afterEach(inject(
    [HttpTestingController],
    (_backend: HttpTestingController) => {
      _backend.verify();
    }
  ));

"узнать больше"

Использование Marble testing. Наконец, большинство сервисов, которые вы создадите в своих проектах angular, будут использовать RxJ. Для правильного тестирования ваших сервисов и компонентов логического контейнера важно понимать, как тестировать эти Observables (лучше всего с помощью jasmine-marbles).

Вот отличная статья Майкла Хоффмана, которая поможет вам лучше понять то же самое

Образец услуги

@Injectable({
  providedIn: 'root'
})
export class ListingService {
  constructor(
    public http: HttpClient
  ) { }
  public getHeaderWithoutToken() {
    return new HttpHeaders()
      .append('Content-Type', 'application/json')
      .append('Accept', 'application/json');
  }
  public getHeader(tokenPrefix = '') {
    let headers = this.getHeaderWithoutToken();
    return { headers };
  }
  public doGet(url,header=this.getHeader()){
    return this.http.get(url,header);
  }
  public getList() : Observable<UserModel[]>{
    return this.doGet('http://example.com/users')
    .pipe(
      map((res:any[])=>{
        return res.map(toUserModel)
    }))
  }
}

Тестирование сервиса с помощью шутки

describe('ListingServiceService', () => {
  let service: ListingService;
  let backend: HttpTestingController;
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule, HttpClientTestingModule],
      providers: [
        ListingService
      ],
      schemas:[NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA]
    });
    backend = TestBed.get(HttpTestingController);
    service = TestBed.get(ListingService);
  });
  afterEach(inject(
    [HttpTestingController],
    (_backend: HttpTestingController) => {
      _backend.verify();
    }
  ));
  it('should be created', () => {
    expect(service).toBeTruthy();
  });
  const url = 'http://example.com/users';
  test('should fetch a list of users',done=>{
    service.getList()
    .subscribe(data=>{
      expect(data).toEqual(outputArray)
      done()
    })
    backend.expectOne((req: HttpRequest<any>) => {
        return req.url === url && req.method === 'GET';
      }, `GET all list data from ${url}`)
      .flush(outputArray);
  })
});

Написание модульных тестов для компонентов контейнера

Компоненты контейнера - это сложные компоненты, и часто эта сложность может привести к путанице при написании модульных тестов для компонента контейнера. Чтобы избежать этого, вы можете использовать подход поверхностного и глубокого тестирования при написании модульных тестов.

Подробнее об этом подходе можно узнать здесь

Пример компонента контейнера приложения

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
  title = 'my-test-app';
  list$ : Observable<UserModel[]>;
  constructor(
    private listService :ListingService,
  ){
  }
  ngOnInit(){
    this.initListService()
  }
  initListService(){
    this.list$ =  this.listService.getList();
  }
  onClicked(user){
  }
}

Настройка контейнера для модульных тестов

let fixture : ComponentFixture<AppComponent>;
  let service : ListingService;
  let component : AppComponent;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      providers:[
        {provide:ListingService,useClass:MockListService}
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
    }).compileComponents();
  }));
  beforeEach(()=>{
    fixture = TestBed.createComponent(AppComponent)
    component = fixture.debugElement.componentInstance;
    service = fixture.componentRef.injector.get(ListingService);
    fixture.detectChanges()
  })

Написание неглубоких тестов

Модульные тесты для тестирования только тех частей, которые изолированы от других компонентов в текущем контейнере, например, все ли компоненты DOM, написанные как часть шаблона этого компонента, отображаются должным образом, компонент настраивается путем выборки данных из служб, и выходные данные компонентов работают как предполагалось.

it('should create the app', () => {
    expect(component).toBeTruthy();
  });

  it('should render title in a h1 tag',() => {
    const compiled = fixture.debugElement.nativeElement;
    expect(queryByTestId(compiled,'app-title')).not.toBeNull();
    expect(queryByTestId(compiled,'app-title').textContent).toEqual(component.title)
  });
  test('should fetch the user list from the listing service',async(()=>{
    const spy = jest.spyOn(service,'getList');
    var expectedObservable = cold('-a',{a:outputArray})
    spy.mockReturnValue(expectedObservable)
    component.ngOnInit()
    fixture.detectChanges()
    expect(spy).toHaveBeenCalled();
    expect(component.list$).toBeObservable(expectedObservable)
    getTestScheduler().flush()
    fixture.detectChanges()
    component.list$.subscribe((o)=>{
      fixture.detectChanges()
      var list = fixture.nativeElement.querySelectorAll('app-list-row-component')
      expect(list.length).toEqual(outputArray.length)
    })
    spy.mockRestore()
  }))

Написание глубоких тестов

Набор модульных тестов, целью которых является проверка взаимодействия в компоненте между дочерними / внутренними компонентами и поставщиками и диспетчерами, прикрепленными к компоненту.

test('should call onClicked when app-list-row-component is clicked',()=>{
    const spy = jest.spyOn(service,'getList');
    var expectedObservable = cold('a',{a:outputArray})
    spy.mockReturnValue(expectedObservable)
    component.initListService()
    getTestScheduler().flush()
    var onClicked = spyOn(component,'onClicked').and.callThrough();
    component.list$.subscribe((o)=>{
      fixture.detectChanges()
      var row0 = fixture.debugElement.query((el)=>{
        return el.properties['data-testid'] === 'row0'
      }).componentInstance as ListRowComponentComponent
      row0.rowClick.emit();
      expect(onClicked).toHaveBeenCalledWith(outputArray[0])
    })
  })

Заключение

В этой статье я надеюсь дать читателю краткое представление о ключевых концепциях, необходимых для интеграции модульного тестирования в ваш код Frontend, а также несколько советов о том, как писать модульные тесты для сложных компонентов и как вы должны разрабатывать свое приложение, чтобы оно стало легко поддерживать здоровую кодовую базу.

Вы можете найти весь код для примера приложения, использованного в этом посте, здесь

Не стесняйтесь разветвлять и практиковать модульное тестирование, используя эту настройку.

АВТОР:

Дивье Марва - Разработчик полного стека | Разработчик программного обеспечения в Zeotap