📜 ⬆️ ⬇️

Jasmine DRY: Do you write tests correctly?

In the interim between the retraining from the Back-end programmer to the Front-end, I had to write code for the RoR application (yes, and there were tests). The peculiar atmosphere of the Rubist community seemed interesting to me, they are very strict about writing code, and if you write badly, your fingers may not be forgiven. After all, the code should be as simple and readable .

The same rule applies to tests (as for me, they should be an order of magnitude simpler than the code itself). In addition, the tests have their own golden rule - One Expectation per Test . No need to write a bunch of expect / assert / should calls in one test, just stop doing it! And do not forget that tests are also code, and copy-paste is bad practice.

What is a bad test


Understanding the 3.0 version of Knockout.js, I decided to look at the tests in the hope of finding at least some mention of the after property inside the binding. Honestly, I was outraged by the complexity of the written tests.

Bad test
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); }); }); 


If you do not take into account that all the directives ( describe and it ) are part of specs, then it is impossible to understand the meaning of the test from the header ( it triggering a click should ... ). It turns out after all nonsense, both in the title and in the test itself.
')
Here is a list of questions that help me create understandable and simple specs:
  1. What are test data?
  2. What is the testing context?
  3. What cases need to cover?
  4. How can these cases be grouped?

For the above example:
  1. Checkbox
  2. User clicks on checkbox
  3. Cases:
    1. The state changes until the click handler is called.
    2. The state changes to marked if the checkbox was not checked.
    3. The state changes to unchecked if the checkbox has been checked.


Now everything is the same in English only:

Just readable test
 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 is complex, tests are simple. The ideal variant is the test in which there is one call of f-tsija expect function .

Less code, more tests


When I first met Jasmine, I realized that she was not perfect, but not finding the possibility of creating group specs, I rushed in panic for help to Google . To my great disappointment, he also did not know the answer that would suit me. I had to dig in the dark depths of Jasmine myself and find a solution.

Let's imagine that there is JavaScript ++, in which there are 2 classes Array and Set with a common interface ( size and contains ). Now you need to cover them with tests, without duplicating the code! Define common tests for our collections:
 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); }) }) 

By analogy to the Rspec , I would like to be able to connect the specs using one of the methods:

Note : itShouldBehaveLike and includeExamplesFor - exist only to improve the readability of tests

 // 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 }); 

I also usually create a context function (elias for describe ) to improve the readability of the spec.

The source code of the shared spec implementation
  // 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; 

Source: https://habr.com/ru/post/207794/


All Articles