Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests #53

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
node_modules
hermione-html-report
23 changes: 23 additions & 0 deletions .hermione.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = {
baseUrl: 'http://localhost:3000',
browsers: {
chrome: {
desiredCapabilities: {
browserName: 'chrome'
}
},
firefox: {
desiredCapabilities: {
browserName: 'firefox'
}
}
},
plugins: {
'html-reporter/hermione': {
path: 'hermione-html-report',
enabled: true
}
},
screenshotsDir: 'hermione-html-report/images',
compositeImage: true
}
98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Домашнее задание: автотесты
[Описание задания](#task)
[Решение (модульные тесты)](#solution-unit)
[Решение (интеграционные тесты)](#solution-integration)

# <a name='task'></a>Домашнее задание: автотесты

Вам дано приложение на JavaScript и нужно написать для него автотесты: интеграционные тесты на интерфейс и модульные тесты на серверную часть.

Expand Down Expand Up @@ -31,3 +35,95 @@ npm start
- нужно добавить в README список логических блоков системы и их сценариев
- для каждого блока нужно написать модульные тесты
- если необходимо, выполните рефакторинг, чтобы реорганизовать логические блоки или добавить точки расширения

# <a name='solution-unit'></a>Решение (модульные тесты)
## Логические блоки
В ходе выполнения были определены следующие логические блоки:
* функции в `./utils/git.js`
- `executeGit`
- `gitFileContent`
- `gitFileTree`
- `gitHistory`
* функции в `./utils/navigation.js`
- `buildBreadcrumbs`

Для удобства тестирования каждый логический блок был вынесен в отдельный файл.
`executeGit` был вынесен в `./libs/execute-git.js`, так как он достаточно универсален, остальные оставлены в директории `./utils`, таким образом, файловая структура `./utils` была преобразована в следующую:

`./utils/`
* `git`
- `git-file-content.js`
- `git-file-tree.js`
- `git-history.js`
- `index.js`
* `navigation`
- `build-bread-crumbs.js`
- `build-file-url.js`
- `build-folder-url.js`
- `index.js`

Все вынесеные в отдельные файлы логические блоки были объединены в директории, содержащие index.js, который их экспортирует. Таким образом удалось сохранить первоначальный API для этих модулей. Из navigation будет тестироваться только модуль `build-bread-crumbs.js`, однако, остальные функции так же были вынесены в отдельные файлы с целью сохранения консистентности.

## Тесты
* Рядом с каждым модулем лежит файл его теста с аналогичным названием, но он имеет расширение `.spec.js`.
* Во всех тестах производится тестирование функций на 1-3 различных аргументах.
* Во всех тестах на место внешних зависимостей (если они есть) поставлены заглушки.
* В модуле `./utils/git` производится тестирование логических блоков на то, что:
- они вызывают функцию `executeGit` с корректными параметрами
- они возвращают промис, который резолвится в корректный тип данных
- производится корректный парсинг переданных данных (если таковой присутствует)
* В модуле `./utils/navigation` производится тестирование логического блока `build-bread-crumbs.js` на правильное построение массива хлебных крошек
* В модуле `./libs/execute-git.js` производится тестирование на то, что:
- производится запуск `child_process.execFile`
- передаются корректные аргументы в `child_process.execFile` (это как раз важно, потому что они имеют не ту же самую сигнатуру, что аргументы самого `executeGit`)
- возвращается промис, который резолвится в строку

Стоит заметить, что во многих тестах, где тестируется асинхронный код можно заметить следующую конструкцию:
```javascript
return Promise.resolve()
.then(() => gitFileContent(hash))
.then(() =>
expect(executeGit).toBeCalledWith('git', ['show', hash])
)
```
Она служит для того, чтоб не получать нечитаемых ошибок в тех случаях, когда тест падает из-за того, что по какой-то причине промис не был возвращен. Передача вызова в качестве аргумента в then гарантированно обернет его в промис и прогонит результат через expect, выдав соответствующую тесту ошибку (в данном случае, что функция не была вызвана с нужными аргументами).
Аналогичная конструкция, которая работает совершенно иначе, но схожа по назначению:
```javascript
const fileContent = gitFileContent('')
expect(fileContent && fileContent.then && fileContent.then(res => typeof res))
.resolves.toBe('string')
```
Если функция вернет, что-то, что точно не является промисом (так как нет then), например, `undefined`, произойдет краш теста не с непонятной ошибкой "Cannot read property 'then' of undefined" (потому что такое сообщение может так же указывать и на неполадку с тестом), а с сообщением тест-раннера о том, что тест провален из-за того что на выходе был не промис. (в таком случае уже понятно, что проблема не с тестом, а с тестируемым модулем)

Некоторые тесты, вроде `execute-git.spec.js` могут показаться необязательными, так как там совсем немного кода и всё что проверяемый модуль делает, это промисифицирует `execFile` и вызывает его, однако, в реальной жизни функции могут и разрастаться и если представить, что она разрослась, то она всё ещё должна быть вызывать execFile с аргументами, соответствующими сигнатуре и возвращать промис, резолвящийся в строку.
## Эволюция моков
Изначально заглушки создавались напрямую. Код был изменен таким образом, что зависимость, на которую требовалась заглушка передавалась в функцию в качестве аргумента со значением по умолчанию и внутри тестов уже передавался дополнительный аргумент с функцией-заглушкой вместо основной. Например:
```javascript
/* БЫЛО: */
// ...
function executeGit(cmd, args) {
return new Promise((resolve, reject) => {
execFile(cmd, args, { cwd: REPO }, (err, stdout) => {
// ...
/* СТАЛО: */
// ...
function executeGit(cmd, args, APIFunc = execFile) {
return new Promise((resolve, reject) => {
APIFunc(cmd, args, { cwd: REPO }, (err, stdout) =>
// ...
```
Плюс этого подхода в том, что он сделал тесты простыми для понимания. Однако, минус в том, что это подвергло изменениям API модуля, что, наверное, нежелательно.
Поэтому впоследствие моки создавались для всего файла, а не для отдельной функции, с помощью метода `jest.genMockFromModule`, что позволило вернутся к первоначальному API.

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

# <a name='solution-integration'></a>Решение (модульные тесты)
## Запуск
Открыть 3 инстанса терминала с соответствующими командами:
1. `npm start`
1. `npm run selenium`
1. `npm run hermione`
## Файлы Гермионы
Конфиг расположен в `./.hermione.conf.js`
Файлы тестирования расположены в `./hermione`
Отчет будет выведен в `./hermione-html-report`
23 changes: 23 additions & 0 deletions hermione/core.hermione.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const assert = require('assert')

describe('core', () => {
it('has breadcrumbs', function () {
return this.browser
.url('/')
.isExisting('.breadcrumbs')
.getText('.breadcrumbs')
.then(bc =>
assert(bc, 'HISTORY', 'no breadcrumbs on core page')
)
.assertView('plain', '.breadcrumbs')
})

it('has commits', function () {
return this.browser
.url('/')
.isExisting('.commit')
.then(cmt =>
assert.ok(cmt, 'no commits')
)
})
})
34 changes: 34 additions & 0 deletions hermione/links.hermione.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
describe('links', () => {
it('core -> commit', function () {
return this.browser
.url('/')
.isExisting('.commit__link')
.assertView('plain', '.container')
.click('.commit__link > a')
.isExisting('.content-tree')
})
it('commit -> file', function () {
return this.browser
.url('/files/fe2008bd672282075b9ab64efe838173daede0a3/')
.isExisting('.content')
.assertView('plain', '.container')
.click('.content li > a')
.isExisting('.file-content')
})
it('commit -> folder', function () {
return this.browser
.url('/files/fe2008bd672282075b9ab64efe838173daede0a3/')
.isExisting('.content')
.assertView('plain', '.container')
.click('.content li:nth-child(5) > a')
.isExisting('.content > ul')
})
it('commit --[breadcrumbs]-> core', function () {
return this.browser
.url('/files/fe2008bd672282075b9ab64efe838173daede0a3/')
.isExisting('.breadcrumbs')
.assertView('plain', '.container')
.click('.breadcrumbs > a')
.isExisting('.content-tree')
})
})
17 changes: 17 additions & 0 deletions libs/execute-git.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { resolve } = require('path');
const REPO = resolve('.');

const child_process = require('child_process');

function executeGit(cmd, args, cwd = REPO) {
return new Promise((resolve, reject) => {
child_process.execFile(cmd, args, { cwd }, (err, stdout) => {
if (err) {
reject(err);
}
resolve(stdout.toString());
});
});
}

module.exports = executeGit
47 changes: 47 additions & 0 deletions libs/execute-git.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const executeGit = require('./execute-git')

// Fake child_process
const child_process = require('child_process')
jest.genMockFromModule('child_process')
child_process.execFile = jest.fn().mockImplementation(
function (cmd, args, options, cb) {
setImmediate(() => {
cb(null, 'hypothetical git output')
})
});
const { execFile } = child_process

describe('command calls', () => {
afterEach(() => execFile.mockClear());

it('runs execFile once', () => {
expect.assertions(1)
return Promise.resolve()
.then(() => executeGit('git', [], 'folder'))
.then(() => expect(execFile.mock.calls.length).toBe(1))
})

it('passes correct arguments to execFile', () => {
expect.assertions(4)
const executeGitArgs = [ 'git', [ 'arg1', 'arg2', 0, 1, 2 ], 'folder' ];
const execFileArgs = [ 'git', [ 'arg1', 'arg2', 0, 1, 2 ], { cwd: 'folder' } ];

return Promise.resolve()
.then(() => executeGit(...executeGitArgs))
.then(() => {
const argsCalledWith = execFile.mock.calls[0]
execFileArgs.map((arg, i, a) => expect(argsCalledWith[i]).toEqual(arg))
expect(argsCalledWith[argsCalledWith.length - 1]).toBeInstanceOf(Function)
})
})

it('resolves to string', () => {
const executeGitPromise = executeGit('git', [], 'folder');
expect(
executeGitPromise &&
executeGitPromise.then &&
executeGit('git', [], 'folder')
.then(res => typeof res)
).resolves.toBe('string')
})
})
Loading