Статьи

Расширенные методы модульного тестирования в JavaScript

К настоящему времени все знают о разработке через тестирование и модульном тестировании. Но используете ли вы рамки тестирования в полной мере?


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

Поскольку в этом руководстве рассматриваются некоторые сложные темы, я предполагаю, что вы уже создали модульные тесты и уже знакомы с основами и их терминологией. Если нет, вот отличная статья для начала: Терминология TDD Упрощенная .

Мы будем использовать SinonJS . Это автономная структура, которая предоставляет API для издевательств, заглушек, шпионов и многого другого. Вы можете использовать его с любой платформой тестирования по вашему выбору, но для этого урока мы будем использовать BusterJS , так как он поставляется со встроенным SinonJS.


Чтобы установить BusterJS, просто запустите в терминале следующее: npm install -g buster

Обратите внимание, что вам нужен Node v0.6.3 или новее.

BusterJS требуется buster.js конфигурации buster.js который сообщает Buster, где находятся ваши исходные файлы и файлы тестов.

Создайте файл buster.js и вставьте следующее:

01
02
03
04
05
06
07
08
09
10
11
12
var config = module.exports;
 
config[«Nettuts Tests»] = {
    rootPath: «./»,
    environment: «browser»,
    sources: [
        «src/*.js»
    ],
    tests: [
        «spec/*-test.js»
    ]
}

Теперь мы сказали Buster, что наши тесты можно найти в папке spec , а наш код реализации — в папке src . Вы можете ссылаться на файлы по их именам файлов или использовать подстановочные знаки, как мы сделали здесь. Обратите внимание, что они относятся к указанному нами rootPath .

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

Для запуска теста с Buster сначала необходимо запустить его сервер и подключить его к браузеру. Вы можете сделать это, запустив buster server в своем терминале. Вы должны увидеть следующее:

бустер-сервер

Теперь откройте ваш любимый браузер и укажите его на http: // localhost: 1111 . Вы должны увидеть следующий экран:

Захват-браузер

Нажмите большую кнопку Браузер захвата , чтобы начать захват этого браузера. Ваши тесты теперь будут выполняться в этом браузере, если вы оставите его открытым. Вы можете подключить столько браузеров, сколько захотите, что дает вам возможность тестировать в нескольких браузерах одновременно (да, даже в хороших старых IE)!

Вы также можете подключить BusterJS к PhantomJS для выполнения ваших тестов без браузера, но это выходит за рамки этого руководства.

Обязательно оставьте сервер и вкладку браузера работающими до конца этого урока.

Чтобы запустить ваши тесты, просто введите buster test в новой вкладке / окне терминала. Как только вы добавите несколько тестов, вы увидите вывод, подобный следующему:

Тест-выход

Прежде чем мы погрузимся в насмешки и заглушки, давайте немного поговорим о тестовых двойниках ; Двойной тест — это объект, который выглядит и ведет себя более или менее как реальная вещь. Они используются в тесте, когда использование реального объекта будет затруднено или нежелательно, и они значительно облегчают тестирование.

Это обычно сравнивают с использованием дублера вместо реального актера на сцене фильма.

В этом уроке мы рассмотрим следующие типы двойников:

  • заглушки
  • шпионы
  • издевается

Шпион — это функция, которая записывает все обращения к ней. Он будет отслеживать аргументы, возвращаемые значения, их значение, создаваемые исключения (если есть) и т. Д. Это может быть анонимная функция или оболочка существующей функции. При использовании в качестве обертки он не изменяет основную функцию; исходная функция будет по-прежнему выполняться как обычно.

Вот как вы создаете шпиона:

1
var spy = sinon.spy();

Это создает анонимную функцию, которая записывает аргументы, их значение, исключения и возвращаемые значения для всех вызовов, которые она получает.

1
var spy = sinon.spy(my_function);

Это шпионит за предоставленной функцией.

1
var spy = sinon.spy(object, «method»);

Это создает шпион для object.method и заменяет оригинальный метод шпионом. Шпион по-прежнему выполняет оригинальный метод, но записывает все вызовы.
Вы можете получить доступ к этому шпиону через вновь созданную spy переменную или напрямую вызвав object.method . object.method может быть восстановлен с помощью вызова spy.restore() или object.method.restore() .

Возвращенный шпионский объект имеет следующие методы и свойства:

1
spy.withArgs(arg1[, arg2, …]);

Создает шпиона, который записывает вызовы только тогда, когда полученные аргументы совпадают с переданными в withArgs .

1
spy.callCount

Возвращает количество записанных звонков.

1
spy.called

Возвращает true если шпион был вызван хотя бы один раз.

1
spy.calledOnce

Возвращает true если шпион был вызван ровно один раз.

1
spy.calledWith(arg1, arg2, …);

Возвращает true если шпион был вызван хотя бы один раз с предоставленными аргументами. Это может быть использовано для частичного соответствия. SinonJS будет проверять только предоставленные аргументы против фактических аргументов. Поэтому вызов, который получает предоставленные аргументы (и, возможно, другие), вернет true .

1
spy.threw([exception]);

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

1
var spyCall = spy.getCall(n);

Возвращает n й вызов, сделанный шпиону. У Spy Call есть собственный API, который вы можете найти здесь: Spy Call API

1
spy.args

Массив аргументов, полученных за вызов. spy.args[0] — это массив аргументов, полученных при первом вызове, spy.args[1] — массив аргументов, полученных при втором вызове и т. д.

1
spy.reset()

Сбрасывает состояние шпиона.

Это была лишь небольшая выдержка из доступных вам методов. Для полного списка всех доступных методов API проверьте документацию здесь: Spy API

Теперь давайте рассмотрим пример использования шпиона. В следующем тесте мы проверяем, использует ли jQuery.get() jQuery.ajax() . Мы делаем это, jQuery.ajax() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
buster.testCase(«Spies», {
 
    tearDown: function() {
        jQuery.ajax.restore();
    },
 
    «should call jQuery.ajax when using jQuery.get»: function() {
 
        sinon.spy(jQuery,»ajax»);
 
        jQuery.get(«/user»);
 
        assert(jQuery.ajax.calledOnce);
 
    }
 
});

На оригинальный метод это не влияет, и он все еще выполняется. Мы просто завернули его в шпиона, чтобы записывать звонки.

После запуска теста мы удаляем шпиона из jQuery.ajax , вызывая .restore() .


Заглушка — это двойной тест с предварительно запрограммированным поведением. Заглушки используются для того, чтобы часть кода работала без реальной реализации .

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

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

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

Вот выдержка из API-заглушки Синона:

1
var stub = sinon.stub();

Это создает анонимную функцию-заглушку.

1
var stub = sinon.stub(object, «method»);

Это заменяет object.method функцией-заглушкой. При заглушении существующего метода, подобного этому, оригинальный метод не будет выполняться всякий раз, когда object.method() .

object.method.restore() функцию можно восстановить, вызвав object.method.restore() или stub.restore() .

1
var stub = sinon.stub(obj);

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

1
stub.withArgs(arg1[, arg2, …]);

Заглушки метода только для предоставленных аргументов.

1
stub.returns(value);

Заставляет заглушку возвращать предоставленное value .

1
stub.returnsArg(index);

Заставляет заглушку возвращать аргумент по указанному индексу; stub.returnsArg(0) заставляет заглушку возвращать первый аргумент.

1
stub.throws();

Заставляет заглушку выдавать исключение. При желании вы можете передать тип ошибки, например, stub.throws("TypeError") .

Вы можете найти полную ссылку на API здесь: API-заглушки

Самый простой способ использовать заглушку — создать анонимную функцию заглушки:

01
02
03
04
05
06
07
08
09
10
11
12
13
buster.testCase(«Stubs Example», {
 
    «should demonstrate anonymous stub usage»: function() {
 
        var callback = sinon.stub();
 
        callback.returns(«result»);
 
        assert.equals(callback(), «result»);
 
    }
 
});

Вот другой пример. Это демонстрирует, как заглушить методы, чтобы заставить поток кода идти по определенному пути:

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
32
33
34
35
36
37
38
39
buster.testCase(«User», {
 
    setUp : function() {
 
        this.user = new User({
            name : ‘John’,
            age : 24,
            loves : ‘coffee’
        });
 
    },
 
    tearDown : function() {
 
        Database.saveRecord.restore();
 
    },
 
    «should return `User saved successfully` when save in database is successful»: function() {
 
        sinon.stub(Database, ‘saveRecord’).returns(true);
 
        var result = this.user.save();
 
        assert.equals(result, ‘User saved successfully’);
 
    },
 
    «should return `Error saving user` when save in database fails»: function() {
 
        sinon.stub(Database, ‘saveRecord’).returns(false);
 
        var result = this.user.save();
 
        assert.equals(result, ‘Error saving user’);
 
    }
 
});

В приведенных выше тестах у нас есть класс User который использует класс Database для сохранения данных. Наша цель — проверить, отвечает ли класс User правильным сообщением, когда Database закончит сохранение пользовательских данных. Мы хотим протестировать как хорошие, так и плохие сценарии.

В производственной среде класс Database может выполнять различные действия для сохранения данных (подключаться к реальной базе данных, выполнять некоторые вызовы AJAX и т. Д.), Которые не представляют интереса для этого теста. Это может даже оказать негативное влияние на результаты нашего теста. Если что-то в классе Database повреждено, мы хотим, чтобы собственные модульные тесты класса Database сломались и указали нам проблему. Другие классы, которые используют класс Database в качестве зависимости, все равно должны работать как положено. Поддельные или заглушки зависимости позволяют нам сделать это, что является сильным аргументом для использования их в первую очередь.

В приведенном выше тесте мы используем заглушку для предварительного программирования поведения метода Database.saveRecord() . Это позволяет нам тестировать оба пути кода, которые нам нужны для нашего теста.

После каждого теста мы вызываем .restore() для метода, который мы вставили, чтобы восстановить исходный метод.

В приведенном выше примере мы заглушаем все вызовы Database.saveRecord() . Мы также можем ограничить нашу заглушку вызовами, которые имеют определенный набор аргументов.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
buster.testCase(«Stubs», {
 
    «should stub different behaviour based on arguments»: function() {
 
        var callback = sinon.stub();
 
        // Stub the same method in 3 different ways, based on the arguments
        callback.withArgs(‘success’).returns(true);
        callback.withArgs(‘getOrder’).returns([‘pizza’, ‘icecream’]);
        callback.withArgs(false).throws(«My Error»);
 
        // Verify each stub
        assert( callback(‘success’) );
        assert.equals( callback(‘getOrder’), [‘pizza’, ‘icecream’]);
 
        try {
            callback(false)
        } catch(e) {}
 
        assert( callback.threw(«My Error»), «Exception ‘My Error’ was not thrown» );
 
    }
 
});

Макеты — это окурки с заранее запрограммированными ожиданиями . Они позволяют вам проверять поведение части программного обеспечения, в отличие от проверки состояния чего-либо, как вы делаете с обычными утверждениями.

Вот список Mock API Sinon:

1
var mock = sinon.mock(obj);

Это создает макет для предоставленного объекта. Он не изменяет объект, но возвращает фиктивный объект, чтобы установить ожидания для методов объекта.

1
var expectation = mock.expects(«method»);

Это переопределяет obj.method с помощью фиктивной функции и возвращает ее. Ожидания приходят с собственным API, о котором мы расскажем позже.

1
mock.restore();

Восстанавливает все ложные методы в их первоначальные функции.

1
mock.verify();

Проверяет все ожидания на макете. Если какое-либо ожидание не оправдывается, возникает исключение. Это также вернет имитируемым методам их первоначальные функции.

Mocks также реализует полный API-заглушку.

Теперь давайте посмотрим, как мы можем реализовать это в примере User мы использовали ранее, когда говорили о заглушках.

Помните, как он использовал метод Database.saveRecord ? Мы никогда не писали тест, чтобы убедиться, что класс User действительно вызывает этот метод правильно, мы просто предполагали, что так и будет.

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

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
buster.testCase(«User», {
 
    setUp : function() {
 
        var userdata = this.userdata = {
            name : ‘John’,
            age : 24,
            loves : ‘coffee’
        };
 
        this.user = new User(userdata);
 
    },
 
    «should use Database class to save userdata»: function() {
 
        var mock = sinon.mock(Database);
 
        mock
            .expects(‘saveRecord’)
            .withExactArgs(this.userdata)
            .once();
 
        this.user.save();
 
        mock.verify();
 
    }
 
});

Как видите, мы saveRecord объект Database и явно указали, как мы ожидаем, что будет saveRecord метод saveRecord . В этом тесте мы ожидаем, что метод будет вызван только один раз, а объект userdata — единственным параметром.

Поскольку наши ожидания уже находятся в нашем макете, нам не нужно писать никаких утверждений, вместо этого мы просто сообщаем макету, чтобы проверить его ожидания с помощью mock.verify() .

Если макет вызывался более одного раза или с параметрами, отличными от указанных нами, он выдаст ошибку, которая приведет к неудаче теста:

макет не удался-проверка

Давайте посмотрим на другой пример, где насмешки могут пригодиться.

Если вы раньше работали с модульными тестами в системе PubSub, вы, вероятно, видели нечто похожее на следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
«should execute subscribers with correct data»: function() {
 
    var pubsub = new PubSub(),
        called = false,
        eventdata = { foo : ‘bar’ },
        callback = function(data) {
            called = (data === eventdata);
        };
 
    pubsub.subscribe(«message», callback);
    pubsub.publish(«message», eventdata);
 
    assert(called);
 
}

Этот тест подтверждает, что подписчик вызывается при публикации события.

Функция callback действует более или менее как макет, поскольку она проверяет, была ли она вызвана с правильными аргументами. Давайте улучшим тест, превратив callback в настоящий макет:

01
02
03
04
05
06
07
08
09
10
11
12
«should execute subscribers with correct data (using mocks)»: function() {
 
    var pubsub = new PubSub(),
        eventdata = { foo : ‘bar’ },
        callback = sinon.mock().withExactArgs(eventdata).once();
 
    pubsub.subscribe(«message», callback);
    pubsub.publish(«message», eventdata);
 
    callback.verify();
 
}

Проще простого. И это также улучшило читабельность теста!

.once() и .withExactArgs() использованные выше, являются ожиданиями . Sinon предлагает массу разных ожиданий, которые вы можете использовать для своих издевательств. Вот некоторые из моих фаворитов:

1
expectation.atLeast(n)

Ожидайте, что метод будет вызван минимум n раз.

1
expectation.atMost(n)

Ожидайте, что метод будет вызван максимум n раз.

1
expectation.never()

Ожидайте, что метод никогда не будет вызван.

1
expectation.once()

Ожидайте, что метод будет вызван ровно один раз.

1
expectation.exactly(n)

Ожидайте, что метод будет вызван ровно n раз.

1
expectation.withArgs(arg1, arg2, …)

Ожидайте, что метод будет вызван с предоставленными аргументами и, возможно, другими.

1
expectation.withExactArgs(arg1, arg2, …)

Ожидайте, что метод будет вызван с предоставленными аргументами, и никакими другими .

1
expectation.verify()

Проверяет ожидание и выдает исключение, если оно не выполнено.

Полный список ожиданий можно найти здесь: API ожиданий

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

1
2
3
4
5
sinon.mock(obj)
    .expects(‘method’)
    .withExactArgs(data)
    .atLeast(1)
    .atMost(3);

Кроме того, вы можете установить ожидания для нескольких методов на одном макете одновременно:

1
2
3
4
5
6
7
8
9
var mock = sinon.mock(obj);
 
mock.expects(‘method1’)
    .atLeast(1)
    .atMost(3);
 
mock.expects(‘method2’)
    .withArgs(data)
    .once();

Или даже установить несколько ожиданий на один и тот же метод:

01
02
03
04
05
06
07
08
09
10
var mock = sinon.mock(obj);
 
mock.expects(‘myMethod’)
    .withArgs(‘foo’)
    .atLeast(1)
    .atMost(3);
     
mock.expects(‘myMethod’)
    .withArgs(‘bar’)
    .exactly(4);

Оба теста должны быть выполнены, чтобы пройти тест.

Теперь, когда мы рассмотрели тестовые пары, давайте поговорим о чем-то совершенно ином, но не менее удивительном: путешествия во времени !


Я не всегда гну время и пространство в модульных тестах, но когда я это делаю, я использую Buster.JS + Sinon.JS ~ Brian Cavalier, Cujo.JS

Вы часто используете setTimeout , clearTimeout , setInterval или clearInterval чтобы задержать выполнение фрагмента кода? Если так, то вы, вероятно, сталкивались с такими тестами:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
buster.testCase(«EggTimer», {
 
    «should execute callback method after 5000ms»: function(done) {
 
        // Overwrite BusterJS default test timeout of 250ms
        this.timeout = 6000;
 
        var mock = sinon.mock().once();
 
        EggTimer.start(5000, mock);
 
        setTimeout(function() {
            mock.verify();
 
            // Because of the asynchronous nature of setTimeout,
            // we need to tell BusterJS when our test is done:
            done();
        }, 5001);
 
    }
 
});

Этот тест проверяет, выполняет ли метод EggTimer.start обратный вызов по истечении определенного периода времени. Но при этом он заставляет вас ждать пять с лишним секунд каждый раз, когда вы запускаете тест !

Представьте себе десять тестов, которые полагаются на setTimeout таким образом; Ваш набор тестов быстро станет настолько медленным, что вы начнете ненавидеть его запускать.

К счастью, SinonJS предоставляет фальшивые таймеры, которые позволяют нам перекрывать часы браузера и перемещаться во времени — Великий Скотт!

Мы можем сделать это с помощью sinon.useFakeTimers() . Таким образом, SinonJS создаст объект часов и переопределит функции таймера браузера по умолчанию своими собственными.

Возвращенный объект часов имеет только два метода:

1
clock.tick(time)

Отметьте время вперед на миллисекунды. Это приводит к выполнению всех таймеров, запланированных на указанный период времени.

1
clock.restore()

Этот вызов обычно выполняется на этапе tearDown теста (suite). Он сбрасывает функции таймера обратно на родные функции браузера.

Теперь, когда мы знаем о фальшивых таймерах, давайте посмотрим, как мы можем использовать их для переписывания вышеуказанного теста:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
buster.testCase(«EggTimer (with fake timers)», {
 
    setUp: function () {
        this.clock = sinon.useFakeTimers();
    },
 
    tearDown: function () {
        this.clock.restore();
    },
 
    «should execute callback method after 5000ms»: function() {
 
        var mock = sinon.mock().once();
 
        EggTimer.start(5000, mock);
        this.clock.tick(5001);
 
        mock.verify();
 
    }
 
});

Сначала мы добавили setUp и tearDown для переопределения и восстановления часов браузера до и после каждого теста.

Затем мы использовали метод clock.tick() для перемещения во времени. Поскольку фальшивые таймеры SinonJS являются синхронными реализациями, нам больше не нужен вызов done() . В качестве дополнительного преимущества наш тест теперь намного легче читать.

Вот сравнение скорости:

поддельные таймеры выдержки сравнение

Наш переписанный тест сокращает общее время выполнения теста с 5012 мс до 12 мс! Мы сохранили ровно 5000 мс, это было значение, которое мы использовали в первом вызове setTimeout() теста!

При использовании фальшивых таймеров, проведение десяти из этих тестов не составляет большого труда. Это только увеличит общее время выполнения теста на несколько миллисекунд, в отличие от 5000 мс на добавленный тест!

Более подробную информацию о часах и функциях таймера Sinon можно найти здесь: Clock API


Мы рассмотрели различные передовые методы, которые вы можете использовать в своих модульных тестах JavaScript. Мы обсуждали шпионов , окурков , насмешек и как подделать функции таймера браузера .

Для этого мы использовали SinonJS, но большинство других сред тестирования (например, Jasmine) поддерживают эти функции (хотя и с собственным API).

Если вы заинтересованы в более глубоких знаниях по модульному тестированию в JavaScript, я настоятельно рекомендую книгу « Разработка через тестирование JavaScript » Кристиана Йохансена (создателя SinonJS).

Я надеюсь, что эта статья была полезна, и что вы изучили некоторые новые методы, которые вы можете использовать при следующем написании модульного теста. Спасибо за чтение.