Статьи

Тестирование HTTP-макетов в Node.js

Конечный продукт
Что вы будете создавать

Пробные тесты HTTP позволяют реализовать и протестировать функцию, даже если зависимые сервисы еще не реализованы. Кроме того, вы можете тестировать сервисы, которые уже были реализованы, фактически не вызывая их, другими словами, вы можете тестировать их в автономном режиме. Вы должны сначала проанализировать свои бизнес-потребности для этого приложения и записать их в виде сценариев в своей проектной документации.

Допустим, вы разрабатываете клиентский API-интерфейс Node.js для медиа-клиента, который является сервисом (такого сервиса нет в реальной жизни, только для тестирования) для музыки, видео, изображений и т. Д. В нем будет много функций этот API, и может возникнуть необходимость в некоторых изменениях время от времени.

Если у вас нет тестов для этого API, вы не будете знать, какие проблемы это вызовет. Однако, если у вас есть тесты для этого API, вы можете обнаружить проблемы, выполнив все тесты, которые вы написали ранее. Если вы разрабатываете новую функцию, вам нужно добавить определенные тестовые сценарии для этого. Вы можете найти API, который уже был реализован в этом репозитории GitHub . Вы можете скачать проект и запустить npm test , чтобы запустить все тесты. Давайте продолжим тестовую часть.

Для тестового макета HTTP мы будем использовать nock , который является библиотекой HTTP-макетов и ожиданий для Node.js. В пробном HTTP-тестировании вы можете применить следующий поток:

  1. Определите правило для запроса / ответа.
  2. Создайте тест и используйте свою функцию в тесте.
  3. Сравните ожидаемые результаты с фактическими результатами в тестовом обратном вызове.

Давайте подумаем об API медиа-клиента. Допустим, вы вызываете функцию musicsList , используя экземпляр библиотеки Media как musicsList ниже:

1
2
3
4
5
var Media = require(‘../lib/media’);
var mediaClient = new Media(«your_token_here»);
mediaClient.musicsList(function(error, response) {
    console.logs(response);
})

В этом случае вы получите список музыки в переменной response , сделав запрос к https://ap.example.com/musics внутри этой функции. Если вы хотите написать для этого, вам нужно смоделировать запросы, чтобы ваши тесты работали в автономном режиме. Давайте смоделируем этот запрос в nock.

01
02
03
04
05
06
07
08
09
10
11
describe(‘Music Tests’, function () {
    it(‘should list music’, function (done) {
        nock(‘https://api.example.com’)
            .get(‘/musics’)
            .reply(200, ‘OK’);
        mediaClient.musicList(function (error, response) {
            expect(response).to.eql(‘OK’)
            done()
        })
    })
})

describe('Musics Tests', function() ..... предназначена для группировки ваших тестов. В приведенном выше примере мы группируем связанные с музыкой тесты в рамках одной группы. it('should list music', function(done).... предназначен для тестирования определенных действий в функциях, связанных с музыкой. В каждом тесте предоставляется обратный вызов done для проверки результата теста внутри функции обратного вызова реальной функции. В запросе mock мы предполагаем, что он ответит OK если мы вызываем функцию musicList . Ожидаемый и фактический результат сравнивается внутри функции обратного вызова.

Вы можете определить данные запроса и ответа в файле. Вы можете увидеть приведенный ниже пример для сопоставления данных ответа из файла.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
it(‘should create music and respond as in resource file’, function (done) {
           nock(‘https://api.example.com’)
               .post(‘/musics’, {
                   title: ‘Smoke on the water’,
                   author: ‘Deep Purple’,
                   duration: ‘5.40 min.’
               })
               .reply(200, function (uri, requestBody) {
                   return fs.createReadStream(path.normalize(__dirname + ‘/resources/new_music_response.json’, ‘utf8’))
               });
           mediaClient.musicCreate({
               title: ‘Smoke on the water’,
               author: ‘Deep Purple’,
               duration: ‘5.40 min.’
           }, function (error, response) {
               expect(JSON.parse(response).music.id).to.eql(3164495)
               done()
           })
       })

Когда вы создаете музыкальное произведение, ответ должен совпадать с ответом из файла.

Вы также можете связать фиктивный запрос в одной области, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
it(‘it should create music and then delete’, function(done) {
           nock(‘https://api.example.com’)
               .post(‘/musics’, {
                   title: ‘Maps’,
                   author: ‘Maroon5’,
                   duration: ‘5:00 min.’
               })
               .reply(200, {
                   music: {
                       id: 3164494,
                       title: ‘Maps’,
                       author: ‘Maroon5’,
                       duration: ‘7:00 min.’
                   }
               })
               .delete(‘/musics/’ + 3164494)
               .reply(200, ‘Music deleted’)
 
           mediaClient.musicCreate({
               title: ‘Maps’,
               author: ‘Maroon5’,
               duration: ‘5:00 min.’
           }, function (error, response) {
               var musicId = JSON.parse(response).music.id
               expect(musicId).to.eql(3164494)
               mediaClient.musicDelete(musicId, function(error, response) {
                   expect(response).to.eql(‘Music deleted’)
                   done()
               })
           })
       })

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

В медиа-клиенте Content-Type: application/json предоставляется в заголовках запросов. Вы можете проверить определенный заголовок, как это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
it(‘should provide token in header’, function (done) {
    nock(‘https://api.example.com’, {
        reqheaders: {
            ‘Content-Type’: ‘application/json’
        }
    })
        .get(‘/musics’)
        .reply(200, ‘OK’)
 
    mediaClient.musicList(function(error, response) {
        expect(response).to.eql(‘OK’)
        done()
    })
 
})

Когда вы делаете запрос на https://api.example.com/musics с заголовком Content-Type: application/json , он будет перехвачен приведенным выше макетом HTTP nock, и вы сможете проверить ожидаемый и фактический результат. Вы также можете проверить заголовки ответа таким же образом, просто указав заголовок ответа в разделе ответа:

01
02
03
04
05
06
07
08
09
10
11
12
13
it(‘should provide specific header in response’, function (done) {
    nock(‘https://api.example.com’)
        .get(‘/musics’)
        .reply(200, ‘OK’, {
            ‘Content-Type’: ‘application/json’
        })
 
    mediaClient.musicList(function(error, response) {
        expect(response).to.eql(‘OK’)
        done()
    })
 
})

Вы можете указать заголовки ответов по умолчанию для запросов в одной области, используя defaultReplyHeaders следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
it(‘should provide default response header’, function(done) {
    nock(‘https://api.example.com’)
        .defaultReplyHeaders({
            ‘Content-Type’: ‘application/json’
        })
        .get(‘/musics’)
        .reply(200, ‘OK, with default response headers’)
 
    mediaClient.musicList(function(error, response) {
        expect(response).to.eql(‘OK, with default response headers’)
        done()
    })
})

Когда вы вызываете API, вы найдете заголовки ответов по умолчанию в ответе, даже если он не указан.

Операции HTTP являются основой клиентских запросов. Вы можете перехватить любую HTTP-операцию, используя intercept :

1
2
3
scope(‘http://api.example.com’)
 .intercept(‘/musics/1’, ‘DELETE’)
 .reply(404);

Когда вы пытаетесь удалить музыку с идентификатором 1, он ответит 404. Вы можете сравнить свой фактический результат с 404, чтобы проверить статус теста. Также вы можете использовать GET , POST , DELETE , PATCH , PUT и HEAD таким же образом.

Обычно фиктивные запросы, сделанные с помощью nock, доступны впервые. Вы можете сделать его доступным столько, сколько хотите:

1
2
3
4
5
6
7
8
var scope = nock(‘https://api.example.com’)
                .get(‘/musics’)
                .times(2)
                .reply(200, ‘OK with music list’);
                 
http.get(‘https://api.example.com/musics’);
http.get(‘https://apiexample.com/musics’);
http.get(‘https://apiexample.com/musics’);

Вы ограничены в выполнении двух запросов в качестве ложных HTTP-запросов. Когда вы делаете три или более запросов, вы автоматически делаете реальный запрос к API.

Вы также можете указать собственный порт в URL вашего API:

1
2
3
4
5
6
7
8
9
it(‘should handle specific port’, function(done) {
    nock(‘https://api.example.com:8081’)
        .get(‘/’)
        .reply(200, ‘OK with custom port’)
    request(‘https://api.example.com:8081’, function(error, response, body) {
        expect(body).to.eql(‘OK with custom port’)
        done()
    })
})

Фильтрация области становится важной, когда вы хотите отфильтровать домен, протокол и порт для ваших тестов. Например, приведенный ниже тест позволит вам смоделировать запросы с поддоменами, такими как API0, API1, API2 и т. Д.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
it(‘should also support sub domains’, function(done) {
    nock(‘http://api.example.com’, {
        filteringScope: function(scope) {
            return /^http:\/\/api[0-9]*.example.com/.test(scope);
        }
    })
        .get(‘/musics’)
        .reply(200, ‘OK with dynamic subdomains’)
 
    request(‘http://api2.example.com/musics’, function(error, response, body) {
        expect(body).to.eql(‘OK with dynamic subdomains’)
        done()
    })
})

Вы не ограничены одним URL здесь; Вы можете протестировать субдомены, указанные в regexp .

Иногда параметры вашего URL могут отличаться. Например, параметры пагинации не являются статичными. В этом случае вы можете использовать следующий тест:

01
02
03
04
05
06
07
08
09
10
11
it(‘should support dynamic pagination’, function(done) {
    nock(‘http://api.example.com’)
        .filteringPath(/page=[^&]*/g, ‘page=123’)
        .get(‘/musics?page=123’)
        .reply(200, ‘Ok response with paginate’)
 
    request(‘http://api.example.com/musics?page=13’, function(error, response, body) {
        expect(body).to.eql(‘Ok response with paginate’)
        done()
    })
})

Если у вас есть различные поля в вашем теле запроса, вы можете использовать filteringRequestBody чтобы исключить такие различные поля:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
it(‘should create movie with dynamic title’, function(done) {
    nock(‘http://api.example.com’)
        .filteringRequestBody(function(path) {
            return ‘test’
        })
        .post(‘/musics’, ‘test’)
        .reply(201, ‘OK’);
 
    var options = {
        url: ‘http://api.example.com/musics’,
        method: ‘POST’,
        body: ‘author=test_author&title=test’
    }
 
    request(options, function(err, response, body) {
        expect(body).to.eql(‘OK’)
        done()
    })
})

Здесь author может отличаться, но вы можете перехватить тело запроса с помощью title=test .

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

01
02
03
04
05
06
07
08
09
10
11
it(‘should match bearer token header’, function(done) {
    nock(‘https://api.example.com’)
        .matchHeader(‘Authorization’, /Bearer.*/)
        .get(‘/musics’)
        .reply(200, ‘Ok response with music list’)
 
    mediaClient.musicList(function(error, response) {
        expect(response).to.eql(‘Ok response with music list’)
        done()
    })
})

В тестовых случаях вы можете включить реальные HTTP-запросы, установив allowUnmocked как true. Давайте посмотрим на следующий случай:

1
2
3
4
5
6
7
it(‘should request performed to «http://api.example.com»‘, function(done) {
    var scope = nock(‘http://api.example.com’, {allowUnmocked: true})
        .get(‘/musics’)
        .reply(200, ‘OK’);
    http.get(‘http://api.example.com/musics’);
    http.get(‘http://api.example.com/videos’);
})

В сценарии nock вы можете увидеть сценарий для URI /musics , но также если вы сделаете какой-либо URL-адрес отличным от /musics , это будет реальный запрос к указанному URL-адресу вместо неудачного теста.

Мы рассмотрели, как писать сценарии, предоставляя пример запроса, ответа, заголовка и т. Д., А также проверяя фактический и ожидаемый результат. Вы также можете использовать некоторые ожидаемые утилиты, такие как isDone() , очистить сценарий области действия с помощью cleanAll() , использовать сценарий cleanAll() навсегда, используя persist() , и перечислить отложенные mocks внутри тестового примера, используя pendingMocks() .

Допустим, вы написали сценарий nock и выполнили реальную функцию для тестового сценария. Внутри вашего тестового примера вы можете проверить, выполняется ли написанный сценарий или нет следующим образом:

1
2
3
4
5
6
7
8
9
it(‘should request performed to «http://api.example.com»‘, function(done) {
    var musicList = nock(‘http://api.example.com’)
        .get(‘/musics’)
        .reply(200, ‘OK with music list’);
    request(‘http://api.example.com/musics’, function(error, response){
        expect(musicList.isDone()).to.eql(true)
        done()
    })
})

Внутри обратного вызова запроса мы ожидаем, что URL в сценарии nock ( http://api.example.com/musics ) будет вызван.

Если вы используете сценарий nock с persist() , он будет доступен навсегда во время выполнения теста. Вы можете очистить сценарий в любое время, используя эту команду, как показано в следующем примере:

01
02
03
04
05
06
07
08
09
10
it(‘should failed due to scope clearing’, function(done) {
    var musicList = nock(‘http://api.example.com’)
        .get(‘/musics’)
        .reply(200, ‘OK with music list’);
    nock.cleanAll();
    request(‘http://api.example.com/musics’, function(error, response, body){
        expect(body).not.eql(‘OK with music list’)
        done()
    })
})

В этом тесте наш запрос и ответ соответствуют сценарию. Однако после nock.cleanAll() мы сбрасываем сценарий и не ожидаем результата с 'OK with music list' .

Каждый сценарий nock доступен только в первый раз. Если вы хотите заставить его жить вечно, вы можете использовать такой тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
it(‘should be available for infinite request’, function(done) {
    nock(‘http://api.example.com’)
        .get(‘/musics’)
        .reply(200, ‘OK’)
    // First call
    request(‘https://api.example.com/musics’, function(error, response, body) {
        expect(body).to.eql(‘OK’)
        done()
    })
    // Second call
    request(‘https://api.example.com/musics’, function(error, response, body) {
        expect(body).to.eql(‘OK’)
        done()
    })
    // …….
})

Вы можете перечислить ожидающие сценарии nock, которые еще не выполнены в вашем тесте, следующим образом:

1
2
3
4
5
6
7
8
9
it(‘should log pending mocks’, function(done) {
    var scope = nock(‘http://api.example.com’)
        .persist()
        .get(‘/’)
        .reply(200, ‘OK’);
    if (!scope.isDone()) {
        console.error(‘Waiting mocks: %j’, scope.pendingMocks());
    }
})

Вы также можете увидеть действия, выполненные во время выполнения теста. Вы можете сделать это, используя следующий пример:

01
02
03
04
05
06
07
08
09
10
it(‘should log all the request’, function(done) {
    var musicList = nock(‘http://api.example.com’)
        .log(console.log)
        .get(‘/musics’)
        .reply(200, ‘OK with music list’);
    request(‘http://api.example.com/musics’, function(error, response, body){
        expect(body).eql(‘OK with music list’)
        done()
    })
})

Этот тест сгенерирует вывод, как показано ниже:

1
matching GET http://api.example.com:80/musics to GET http://api.example.com:80/musics: true

Иногда вам нужно отключить макет для определенных URL-адресов. Например, вы можете назвать реальный http://tutsplus.com , а не издеваться над. Ты можешь использовать:

1
nock.nock.enableNetConnect(‘tutsplus.com’);

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

1
2
nock.enableNetConnect();
nock.disableNetConnect();

Обратите внимание, что если вы отключите net connect и попытаетесь получить доступ к немодулированному URL, вы получите исключение NetConnectNotAllowedError в своем тесте.

Лучше всего писать тесты перед реальной реализацией, даже если ваша зависимая служба еще не реализована. Помимо этого, вы должны иметь возможность успешно выполнять свои тесты, когда вы находитесь в автономном режиме. Nock позволяет имитировать ваш запрос и ответ, чтобы протестировать ваше приложение.