Jasmine DRY: а ты правильно пишешь тесты?

от автора

В промежутке времени между переквалификацией с Back-end программиста на Front-end, мне пришлось иногда код для RoR приложения (да-да и тесты были). Интересным для меня показалась своеобразная атмосфера сообщества рубистов, которые очень строго относятся к написанию кода и если ты пишешь плохой код, то тебе могут поломать пальцы не простить. Ведь код должен быть максимально простым и читабельным.

Это же правило применимо и к тестам (как по мне то, они должны быть на порядок проще чем сам код). В дополнение, в тестах есть свое золотое правило — One Expectation per Test. Не нужно писать кучу expect/assert/should вызовов в одном тесте, просто перестаньте это делать! И не забывайте, что тесты это тоже код, а copy-paste — плохая практика.

Что такое плохой тест

Разбираясь в 3.0 версии Knockout.js, я решил посмотреть тесты в надежде разобраться как найти хоть какое-то упоминание о новом свойстве after внутри байндингов. Честно говоря, меня возмутила сложность написанных тестов.

Плохой тест

describe('Binding: Checked', function() {     beforeEach(jasmine.prepareTestNode);      it('Triggering a click should toggle a checkbox\'s checked state before the event handler fires', function() {         testNode.innerHTML = "<input type='checkbox' />";         var clickHandlerFireCount = 0, expectedCheckedStateInHandler;         ko.utils.registerEventHandler(testNode.childNodes[0], "click", function() {             clickHandlerFireCount++;             expect(testNode.childNodes[0].checked).toEqual(expectedCheckedStateInHandler);         })         expect(testNode.childNodes[0].checked).toEqual(false);         expectedCheckedStateInHandler = true;         ko.utils.triggerEvent(testNode.childNodes[0], "click");         expect(testNode.childNodes[0].checked).toEqual(true);         expect(clickHandlerFireCount).toEqual(1);          expectedCheckedStateInHandler = false;         ko.utils.triggerEvent(testNode.childNodes[0], "click");         expect(testNode.childNodes[0].checked).toEqual(false);         expect(clickHandlerFireCount).toEqual(2);     }); }); 

Если не учитывать, что все директивы (describe и it) являются частью спеки, то потом невозможно понять смысл теста из заголовка (it triggering a click should…). Получается ведь бред, как в заголовке так и в самом тесте.

Вот список вопросов, которые помогают мне создавать понятные и простые тесты:

  1. Какие тестовые данные?
  2. Какой контекст тестирования?
  3. Какие кейсы нужно покрыть?
  4. Как можно сгруппировать эти кейсы?

Для выше приведенного примера:

  1. Поле ввода checkbox
  2. Пользователь жмет на checkbox
  3. Кейсы:
    1. Состояние меняется до вызова обработчика клика
    2. Состояние меняется в отмеченный, если checkbox был не отмечен
    3. Состояние меняется в не отмеченный, если checkbox был отмечен

Теперь все то же самое только на английском jasmine-ском:

Просто читаемый тест

describe('Binding: Checked', function() {     beforeEach(jasmine.prepareTestNode);      describe("when user clicks on checkbox", function () {         beforeEach(function () {             testNode.innerHTML = "<input type='checkbox' />";             this.checkbox = testNode.childNodes[0];             this.stateHandler = jasmine.createSpy("checked handler");              this.checkbox.checked = false;             ko.utils.registerEventHandler(this.checkbox, "click", function() {                 this.stateHandler(this.checkbox.checked);             }.bind(this));             ko.utils.triggerEvent(this.checkbox, "click");         })          it ("changes state before event handler is triggered", function () {             expect(this.stateHandler).toHaveBeenCalledWith(true);         })          it ("marks checkbox if it's not marked", function () {             expect(this.checkbox.checked).toBe(true)         })          it ("unmarks checkbox if it's marked", function () {             this.checkbox.checked = true;             ko.utils.triggerEvent(this.checkbox, "click");             expect(this.checkbox.checked).toBe(false);         })     }) }) 

Setup — сложный, тесты — простые. Идеальный вариант — это тест в котором находится один вызов ф-ции expect.

Меньше кода, больше тестов

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

Давайте представим, что существует JavaScript++, в котором есть 2 класса (Array и Set) с общим интерфейсом (size и contains). Теперь нужно покрыть их тестами, без дублирования кода! Определим общие тесты для наших коллекций:

sharedExamplesFor("collection", function () {   beforeEach(function () {      this.sourceItems = [1,2,3];      this.collection = new this.describedClass(this.sourceItems);   })    it ("returns proper size", function () {     expect(this.collection.size()).toBe(this.sourceItems.length);   })    // another specs    it ("returns true if contains item", function () {     expect(this.collection.contains(this.sourceItems[0])).toBe(true);   }) }) 

По аналогии к Rspec, хотелось бы иметь возможность подключать спеки при помощи одного из методов:

  • itBehavesLike — выполняет тесты во вложенном контексте
  • itShouldBehaveLike — выполняет тесты во вложенном контексте
  • includeExamples — выполняет тесты в текущем контексте
  • includeExamplesFor — выполняет тесты в текущем контексте

Note: itShouldBehaveLike и includeExamplesFor — существуют только для улучшения читаемость тестов

// array_spec.js describe("Array", function () {    beforeEach(function () {       this.describedClass = Array;    })     itBehavesLike("collection");    //another specs })  // set_spec.js describe("Set", function () {    beforeEach(function () {       this.describedClass = Set;    })     itBehavesLike("collection");    //another specs }); 

Еще я себе обычно создаю ф-ция context (элиас для describe) для улучшения читабельности спек.

Исходный код реализации shared spec

  // spec_helper.js   var sharedExamples = {};    window.sharedExamplesFor = function (name, executor) {      sharedExamples[name] = executor;   };    window.itBehavesLike = function (sharedExampleName) {       jasmine.getEnv().describe("behaves like " + sharedExampleName, sharedExamples[sharedExampleName]);   };    window.includeExamplesFor = function (sharedExampleName) {       var suite = jasmine.getEnv().currentSuite;       sharedExamples[sharedExampleName].call(suite);   };    window.context = window.describe;   window.includeExamples = window.includeExamplesFor;   window.itShouldBehaveLike = window.itBehavesLike; 

ссылка на оригинал статьи http://habrahabr.ru/post/207794/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *