В промежутке времени между переквалификацией с 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...). Получается ведь бред, как в заголовке так и в самом тесте.
Вот список вопросов, которые помогают мне создавать понятные и простые тесты:
- Какие тестовые данные?
- Какой контекст тестирования?
- Какие кейсы нужно покрыть?
- Как можно сгруппировать эти кейсы?
Для выше приведенного примера:
- Поле ввода 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;
Автор: serjoga