Почему разработчики пропускают написание тестов для некоторого кода? Возможно, потому что он опирается на файловую систему или внешний API. Возможно, потому что мы думаем, что это слишком сложно, или тестовый аккаунт для внешнего API не гарантированно будет доступен. Возможно, это связано с тем, что внешний API может быть недоступен из среды CI из-за ограничений VPN. Возможно, издевательство в вашем тестовом наборе стало настолько сложным, что поддержание насмешек не дает достаточной отдачи для затрат времени. Это может произойти, потому что макетирование слишком тесно связано с тестируемым кодом.
Иногда тестирование каждой ветви кажется непреодолимым препятствием. Знание того, как легко тестировать условия ошибок, значительно облегчает увеличение охвата ветвей.
Лучше издеваться
Модульные тесты изолируют тестируемый код от других систем. Это позволяет вам сосредоточиться на своем коде, а не на взаимодействии с другими системами. Один из способов изолировать тестируемое устройство от других систем — использовать макетирование.
Хорошо имитирует системы, а не подражает поведению тестируемого кода. Ложные серверы, к которым можно получить доступ через локальный хост, часто проще всего использовать. В этом сценарии тестируемый код может остаться неизменным, но конфигурация может быть обновлена, чтобы указать клиентам на фиктивный сервер, работающий на localhost.
Иногда макеты легко доступны в виде модулей NPM. Например, s3rver
это фиктивный сервер для сервиса Amazon S3.
const Aws = require('aws-sdk')
const S3rver = require('s3rver')
const Util = require('util')
const createMockS3Server = () => {
return new Promise((resolve, reject) => {
const mock = new S3rver({
port: 4569,
hostname: 'localhost',
silent: true,
directory: '/tmp/s3rver'
}).run((error) => {
if (error) {
reject(error)
return
}
resolve(mock)
})
})
}
Aws.config.s3 = {
s3ForcePathStyle: true,
endpoint: 'http://localhost:4569'
}
test('starts s3rver', async () => {
const mockS3Server = await createMockS3Server()
await s3Client.createBucket({ Bucket: namespace }).promise()
await testCodeThatUploadsAFileToTheBucket()
await Util.promisify(mockS3Server.close)()
})
К сожалению, во многих ситуациях тестируемая служба еще не может создать созданную версию. yakbak
Модуль может быть полезным инструментом в этой ситуации. Это позволяет записывать ответы HTTP для последующего использования; без необходимости делать дополнительный сетевой запрос.
Это позволяет создавать базовые фиктивные серверы для ваших модульных тестов. Практически во всех проектах можно предположить, что HTTP-модуль Node.js и сетевой интерфейс хорошо протестированы, и дополнительное тестирование их не дает никаких дополнительных преимуществ.
Еще одним преимуществом использования yakbak
является производительность. Тестовые наборы, которые запускаются за меньшее время, могут выполняться чаще. Они могут даже запускаться, когда нет сетевого интерфейса, что означает, что разработчики могут продолжать работать, даже если внешние API недоступны по любой причине.
const Http = require('http')
const Yakbak = require('yakbak')
const allowMockServerRecordings = !process.env.CI
const createMockNpmApi = () => {
return Http.createServer(
Yakbak('https://api.npmjs.org', {
dirname: Path.join(__dirname, 'npm-api-tapes'),
noRecord: !allowMockServerRecordings
})).listen(7357)
}
test('Can extract module info', async () => {
const mockNpmApi = createMockNpmApi()
try {
await doSomethingWithNpm('http://localhost:7357')
}
finally {
await mockNpmApi.close()
}
})
Любые запросы, которые не кэшируются, отправляются на действующий сервер, и новые записи записываются. Они должны быть проверены на достоверность и зафиксированы в хранилище, если запрос ожидается и действителен. Или вы можете заметить ленту для запроса, который вы не ожидали. Если это так, ленту можно удалить и исправить ошибку. Вы можете отслеживать, какие звонки были сделаны, потому что новая лента добавляется для каждого нового запроса.
CI
Переменная среды используется для определения , является ли код , работающий в среде непрерывной интеграции (CI) , таких как CircleCI. В этой среде переменная установлена, и новые ленты не создаются. Вместо этого выдается ошибка. Это предотвращает ложные срабатывания во время тестирования CI для неожиданных запросов. Любые ленты, не ожидаемые в CI, обычно указывают на ошибку или сценарий, не рассматриваемый во время написания теста.
Создание фиктивного сервера с помощью yakbak
может быть основой для создания более абстрактного фиктивного сервера позже. Примером этого является mock-okta
. Этот инструмент состоит из нескольких yakbak
лент, но с одной конечной точкой, имитирующей пользовательский код.
Mocking Modules
Другой подход, который используется, когда готовый макет для зависимости недоступен, или когда зависимость не использует HTTP в качестве средства связи, или когда зависимость, которая должна быть смоделирована, вообще не является сервером, заключается в использовании proxyquire. , Это модуль, который используется в тестах для проверки зависимостей на require
уровне. Когда тестируемый код пытается получить require
внешнюю зависимость, он получает вместо этого проверенную версию:
const Faker = require('faker')
const NoGres = require('@nearform/no-gres')
const Proxyquire = require('proxyquire').noPreserveCache().noCallThru()
test('counting users works', async () => {
const mockPgClient = new NoGres.Client()
mockPgClient.expect('SELECT * FROM users', [],
[{ email: Faker.internet.exampleEmail(), name: Faker.name.firstName() }])
const User = Proxyquire('../user', {
pg: {
// `Client` can't be an arrow function because it is called with `new`
Client: function () {
return mockPgClient
}
}
})
expect(await User.count()).to.equal(1)
mockPgClient.done()
})
Чтобы изолировать состояние от тестовой функции, noPreserveCache()
вызывается так, что каждый раз, когда proxyquire
используется модуль, загружается заново. Обратите внимание, что любые побочные эффекты загрузки модуля возникают в каждом тесте при использовании noPreserveCache()
.
В этом примере noCallThru()
также используется. Это важно, когда неожиданный звонок может быть разрушительным. Например, если оператор SQL DROP TABLE
был случайно выполнен для вашей локальной базы данных Postgres. Использование noCallThru()
вызывает ошибку, если вызваны какие-либо функции, которые не были проверены.
Фейкерный модуль также используется в этом примере. Он имеет большой набор функций, которые можно использовать для создания фиктивных данных, чтобы тесты были более надежными. Например, если в вашем тесте всегда используется фальшивое электронное письмо, например [email protected]
, и ошибка возникает только в том случае, если электронное письмо содержит подчеркивание, эта проблема может быть не обнаружена до следующего выпуска. Используя фейкер, можно протестировать больше форм данных, увеличивая вероятность обнаружения ошибки до выпуска следующей версии программного обеспечения.
Файловая система
Код, который тестирует файловую систему, на первый взгляд может показаться трудным для тестирования, но есть легкодоступный модуль NPM, mock-fs
который облегчает его.
const Fs = require('fs')
const MockFs = require('mock-fs')
test('It loads the file', async () => {
try {
MockFs({
'path/to/fake/dir/some-file.txt': 'file content here'
});
const content = Fs.readSync('path/to/fake/dir/some-file.txt')
expect(content).to.equal('file content here')
}
finally {
MockFs.restore();
}
})
Даты и таймеры
Есть также модуль, который помогает тестировать код, который использует таймеры и другие функции JavaScript, связанные с датами. Это называется лолекс.
const Lolex = require('lolex');
const Sprocket = require('../sprocket')
// Sprocket.longRunningFunction may look something like:
// const longRunningFunction = async () => {
// await new Promise((resolve) => setTimeout(resolve, 900000))
// return true
// }
test('a long running function executes', async () => {
const clock = Lolex.install();
try {
// Date.now() returns 0
const p = Sprocket.longRunningFunction()
clock.tick(900000)
// Date.now() returns 900000
const status = await p
expect(status).to.equal(true)
}
finally {
clock.uninstall()
}
})
В этом примере код longRunningFunction
должен подождать 15 минут, прежде чем вернуть свое значение. Очевидно, что ожидание 15 минут каждый раз, когда longRunningFunction
вызывается из теста, далеко от идеала! Посмеиваясь над глобальной setTimeout
функцией, мы можем вообще избежать ожидания и вместо этого продвинуть часы на 15 минут, чтобы обеспечить возвращение ожидаемого значения.
Покрытие кода
Покрытие кода не показывает, что код выполняется так, как ожидается, как модульные тесты. Покрытие кода показывает, что тестируемый код выполняется, не имеет времени ожидания, не вызывает непредвиденных ошибок или, в случае ошибки, что он перехватывается и обрабатывается, как ожидается.
Хотя существуют разные мнения относительно того, какой уровень покрытия кода желателен и есть ли убывающая отдача при переходе, скажем, с 90% на 100% покрытия кода, многие разработчики согласны с тем, что тесты покрытия кода полезны.
Просто загрузка вашего кода дает вам 60-70% покрытия кода, поэтому мы на самом деле говорим только о покрытии за последние 30-40%. Использование интеллектуального фаззинга может повысить эту начальную точку чуть больше до 100%, при этом с вашей стороны не потребуются дополнительные усилия.
Smart Fuzzing
Согласно MongoDB, они обнаруживают гораздо больше ошибок, прежде чем достигают производства с помощью интеллектуального фаззинга, чем они делают с модульными тестами (см. Https://engineering.mongodb.com/post/mongodbs-javascript-fuzzer-creating-chaos). В то время как MongoDB использует продвинутую технику интеллектуального фаззинга, которая изменяет код модульного теста, нам не нужно заходить так далеко, чтобы извлечь выгоду из интеллектуального фаззинга. Используя встроенную функцию проверки Hapi для определения того, как выглядят запросы и / или ответы, мы не только получаем выгоду от проверки, но и можем использовать эту информацию для запуска интеллектуальных фаззин-тестов.
const createServer = async () => {
const server = Hapi.server({ port: 7357 })
server.route({
method: 'GET',
path: '/{name}/{size}',
handler: (request) => request.params,
options: {
response: {
sample: 10,
schema: Joi.object().keys({
name: Joi.string().min(1).max(10),
size: Joi.number()
})
},
validate: {
params: {
name: Joi.string().min(1).max(10),
size: Joi.number()
}
}
}
})
await server.start()
console.log('Server running at: ${server.info.uri}')
return server
}
test('endpoints do not time out and any errors are caught', async () => {
const testServer = await createServer()
try {
await SmartFuzz(testServer)
}
finally {
await testServer.stop()
}
})
})
В то же количество времени я потратил , пытаясь настроить swagger-test
или got-swag
для использования в файлах «тест .js» с использованием запросов с проверкой подлинности, я уже написал первую итерацию SmartFuzz. Важно иметь инструменты, когда это возможно, которые можно использовать из тестового кода Node.js, поскольку это значительно упрощает создание тестовой среды. Хотя они, вероятно, являются приличными инструментами командной строки, менее удобно, что их нелегко перенести в файлы «test .js».
Тестирование обработки ошибок
Также связанные с покрытием кода, тестирование веток часто является «последней милей» тестирования покрытия. Часто эти ветки являются случаями обработки ошибок, что увеличивает сложность. Тестирование кода в условиях ошибки приводит к тестированию большего количества веток.
Прежде чем мы проверим условия ошибки, короткое примечание. Иногда люди путаются, если во время тестирования выводятся сообщения об ошибках. Тем не менее, важно проверить условия ошибок, даже если они вызывают вывод. Одно из решений состоит в том, чтобы заглушить console.log
и console.error
скрыть сообщения, но я предпочитаю видеть вывод, чтобы помочь отладке любых найденных ошибок. Итак, давайте добавим полезное сообщение для людей, которые могут быть встревожены:
console.warn('⚠️ Note: error message output is expected as part of normal test operation.')
Большинство библиотек утверждений содержат код для проверки ожидаемых отклонений обещаний. По какой-то причине мне не удалось сделать эти работы. На практике я использовал эту функцию:
const expectRejection = async (p, message) => {
try {
await p
} catch (error) {
if (error.message === message) return
throw new Error('Expected error "${message}" but found "${error.message}"')
}
throw new Error('Expected error "${message}" but no error occurred.')
}
Использование этой функции облегчает тестирование ошибок. Когда это легко, нет оправдания тому, чтобы не тестировать еще много ветвей в коде, значительно увеличить охват кода и одновременно тестировать код в то время, когда он наиболее уязвим, когда произошла ошибка.
test('invalid input causes an error', async () => {
await expectRejection(doSomeTest({ input: 'invalid' }), 'Validation failed.')
})
Заключение
Готовы увеличить окупаемость теста? Применяйте эти советы на практике сегодня. Не позволяйте внешним системам, сложным проверкам, тестированию веток или низкому охвату кода мешать!