Не важно, какой подход применяется при написании тестов: TDD, BDD, или какой-то другой. Юнит- тесты это первичный защитный барьер, который помогает избежать багов. А хорошо описанные кейсы помогут коллегам понять, что происходит в проекте и не наломать дров в коде.
Перейдем к сути:
Есть конкретная проблема: 5k+ юнит-тестов проходят за 12 минут — это в два раза больше времени установки пакетов и самой сборки.
Это очень много.
Если прикинуть, сколько времени с каждой сборкой уходит на это в день — становится грустно!
Ковыряние каждого теста на наличие проблем ситуацию не сильно изменит. Тесты выкидывать нельзя, а время сокращать необходимо.
Есть небольшой и удобный плагин 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 минуты
В четыре раза, Карл!!!