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

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 минуты

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

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *