Юнит-тестирование. Чип-тюнинг

в 13:56, , рубрики: angular, javascript, karma, webpack, Блог компании Tinkoff.ru, Тестирование веб-сервисов

image

Не важно, какой подход применяется при написании тестов: TDD, BDD, или какой-то другой. Юнит- тесты это первичный защитный барьер, который помогает избежать багов. А хорошо описанные кейсы помогут коллегам понять, что происходит в проекте и не наломать дров в коде.

Перейдем к сути:

Есть конкретная проблема: 5k+ юнит-тестов проходят за 12 минут — это в два раза больше времени установки пакетов и самой сборки.

Это очень много.

Если прикинуть, сколько времени с каждой сборкой уходит на это в день — становится грустно!

image

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

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

Обычная конфигурация для юнит-тестов в стартере ангуляра с вебпаком и кармой вкратце выглядит так:

Конфиг кармы karma.conf.js устанавливает файлы, которые будет обрабатывать:

files: [
    { pattern: './config/spec-bundle.js', watched: false },
    { pattern: './src/assets/**/*', watched: false, included: false, served: true, nocache: false }
]

далее подключает препроцессор для конфига вебпака webpack.test.js

preprocessors: { './config/spec-bundle.js': ['coverage', 'webpack', 'sourcemap'] }

Заметим, что два раза фигурирует spec-bundle.js файл.

Внутри у этого файла происходит следующее:

Первым делом это установка необходимых зависимостей, без которых наш ангуляр-код не запустится в тестах:

Error.stackTraceLimit = Infinity;

require('core-js/es6');
require('core-js/es7/reflect');

require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy'); // since zone.js 0.6.15
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');

require('rxjs/Rx');

var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing');

testing.TestBed.initTestEnvironment(
  browser.BrowserDynamicTestingModule,
  browser.platformBrowserDynamicTesting()
);

Вторая часть файла — это контекст для файлов юнит-тестов. При сборке webpack загрузит по этому контексту все разрезолвленные файлы по регулярке. Это как раз те самые юнит-тесты, которые запускает карма:

/**
 * Ok, this is kinda crazy. We can use the context method on
 * require that webpack created in order to tell webpack
 * what files we actually want to require or import.
 * Below, context will be a function/object with file names as keys.
 * Using that regex we are saying look in ../src then find
 * any file that ends with spec.ts and get its path. By passing in true
 * we say do this recursively
 */
var testContext = require.context('../src', true, /.spec.ts/);

/**
 * Get all the files, for each file, call the context function
 * that will require the file and load it up here. Context will
 * loop and require those spec files here
 */
function requireAll(requireContext) {
  return requireContext.keys().map(requireContext);
}

/**
 * Requires and returns all modules that match
 */
var modules = requireAll(testContext);

Так как в режиме разработки нам нет необходимости запускать все тесты сразу, достаточно одного модуля, то и проблемы со временем выполнения тестов для такого модуля не нужно. Всё происходит достаточно быстро.

Те самые 12 минут — это продакшн/тест сборки. Рассматриваем именно этот случай.

Для решения проблемы, мы распараллелим все тесты для N браузеров. Подключение плагина karma-sharding не требует больших манипуляций:

Во-первых добавление во фреймворки в конфиге кармы

frameworks: [..., 'sharding']

Во-вторых, это добавление конфига самого плагина

sharding: {
    specMatcher: /(spec|test)s?.js/i,
    base: '/base',
    getSets: function(config, basePath, files) {
        // splitForBrowsers - some util function
        return splitForBrowsers(files.served)
            .map(oneBrowserSet => [someInitScript].concat(oneBrowserSet));
    }
  }

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

[a1.spec.js, a2.spec.js, … aN.spec.js]

то набор для N браузеров выглядит так:

[a1.spec.js], [a2.spec.js], … [aN.spec.js]

а для N/2 соответсвенно так:

[a1.spec.js, a2.spec.js], [a3.spec.js, a4.spec.js], … [aN-1.spec.js, aN.spec.js]

и тогда тут всё просто, но не в нашем случае с Angular и webpack. Один тестируемый файл состоит из двух частей:

1 - необходимые зависимости   //some setup code
2 - наборы юнит-тестов            //require.context('../src', true, /.spec.ts/);

При тестировании всего приложения нам нужно несколько таких файлов с разными наборами тестов, однако сборка таких файлов будет занимать много времени, так как она происходит далеко не моментально. Например, результирующий преобразованный в вебпак-модули код таких необходимых зависимостей приближается к 100k строк.

Но мы будем резать!

То есть каждый такой самостоятельный файл мы разделим на две части: первая — это все необходимые зависимости и настройки — setup.js, а вторая — набор тестов подключенный через контекст вебпака — testsN.js. Так как setup.js — это общий набор установок, одинаковый для всех наборов тестовых кейсов, то такой файл будет один.

В результате у нас должен получиться следующий набор файлов:

setup.js
tests1.js
tests2.js

testsN.js

Которые нам нужно собрать в следующие наборы:

[setup.js, tests1.js], [setup.js, tests2.js], … [setup.js, testsN.js]

Шаг #1

Первым делом пройдемся по всему коду в поисках всех необходимых файлов с юнит-тестами и распределим их в несколько файлов — testsN.js, в зависимости от того, сколько браузеров планируется использовать — некоторое N. Один такой файл, например, тот же tests1.js выглядит так:

require('/Users/guest/test-project/src/modules/accounts/accounts.spec.ts');
require('/Users/guest/test-project/src/modules/cards/cards.spec.ts');
require('/Users/guest/test-project/src/modules/users/users.spec.ts');
...

Распределение кейсов по файлам конечно же может быть реализовано, как душе угодно. В нашем случае это приблизительно равномерное распределение по количеству кейсов в *.spec.ts файле.

Все необходимые файлы собираем в любой удобной для нас папке — некая tmp директория.
После этого мы задаем webpack’у следующие enrties:

entry: {
     entry = fs.readdirSync(path.join(tmp)).reduce((entries, fileName) => {
              entries[fileName] = path.join(tmp, fileName);

              return entries;
      }, {setup: path.join(tmp, ‘setup.ts’)});
},

Важно setup собирать как common chunk, тогда мы сможем его загружать перед каждым набором тестов.

new CommonsChunkPlugin({
       name: 'setup',
       minChunks: module => /setup.test/.test(module.resource)
})

Так после запуска webpack мы получим скомпилированные setup.js и N файлов с тестовыми кейсами. Этот запуск будет первым шагом из двух при запуске тестов.

Шаг #2

Это настройка karma и karma-sharding. Как уже было представлено, настроек не много. Самая интересная — это функция собирающая набор тестовых кейсов getSets:

const {splitArray, isSpecFile} = require('karma-sharding/lib/utils');

… 

function getSets(config, basePath, files) {
   const setupScript = files.served.find(file => file.path.indexOf('setup') > -1);

   const specs = files.served
       .map(file => return config.base + file.path)
       .filter(filePath => isSpecFile(filePath, config.specMatcher) && !/(setup)/.test(filePath));

   return splitArray(specs, config.browserCount).map(set => {
       return set.concat([config.base + setupScript.path.replace(basePath, '')]);
   });
}

Конфиг specMatcher — тут мы находим скомпилированные setup.js и все testsN.js файлы в некоторой директории tmp.

И всё — конфиг для karma готов.
Дальше только запуск webpack для сборки тестов и запуск karma!

Ну, и конечно же цифры:

5k+ юнит-тестов
До: с одним браузером — 12 минут
После: на 10 браузерах — 3 минуты

В четыре раза, Карл!!!

Автор: jbubsk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js