Это же правило применимо и к тестам (как по мне то, они должны быть на порядок проще чем сам код). В дополнение, в тестах есть свое золотое правило — 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…). Получается ведь бред, как в заголовке так и в самом тесте.
Вот список вопросов, которые помогают мне создавать понятные и простые тесты:
- Какие тестовые данные?
- Какой контекст тестирования?
- Какие кейсы нужно покрыть?
- Как можно сгруппировать эти кейсы?
Для выше приведенного примера:
- Поле ввода checkbox
- Пользователь жмет на checkbox
- Кейсы:
- Состояние меняется до вызова обработчика клика
- Состояние меняется в отмеченный, если checkbox был не отмечен
- Состояние меняется в не отмеченный, если 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) для улучшения читабельности спек.
// 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/
Добавить комментарий