Previous: part 3 – Next: part 5
You can also find the code below in the src/unit-test-2 dir of my blog code repository.
Today, you have been asked by your uncle to help him set up the new MegaBanana Slide in his Fun Park. The MegaBanana Slide is meant for children, but not too small children. Certainly not for adults. And you must take off your shoes before using it. Oh, and of course you are not allowed to go together with your friend – each child has to wait for its turn!
To prevent any accidents, the MegaBanana Slide has a sophisticated Child Measurement System, where any child who stands near the start of the Slide is measured. The results of this measurement are then fed into the Permission Granting System, which then opens the gate – or not.
Your uncle knows that you are a star programmer, so he has asked you to program the PGS according to his specifications:
- only 1 child at a time
- no shoes
- no children under 90 cm (that’s about 3 feet)
- absolutely nobody who weighs more than 125 kg (275 lbs)!
The PGS wants an object with a canUse field and a reason field. If canUse is false , the reason is shown to the poor child who is not allowed to go down the slide.
Writing this code takes you all of 3 minutes and 42 seconds.
function canUseSlide(person) { if (Array.isArray(person) && person.length > 1) { // Sneaky! Trying to go with more than 1 person together! return { canUse: false, reason: 'Only 1 person at a time!', }; } if (person.weight > 125) { // We don't want the slide to break! return { canUse: false, reason: 'This slide is for children only.', }; } if (person.height < 90) { // Small children are not allowed on this dangerous slide. return { canUse: false, reason: 'You are not tall enough yet.', }; } if (person.isWearing('shoes')) { // You're only allowed to go down the slide barefoot. return { canUse: false, reason: 'You must take off your shoes first!', }; } return { canUse: true, reason: '' }; }
Easy peasy. But you know that your uncle is a nitpick, so you decide to write some unit tests (even though he has no clue what that term even means) to verify that your code does the right thing.
require('should'); var slide = require('./slide'); describe('canUseSlide', function() { it('should verify if you can use the slide: child the slide is intended for', function() { var result = slide.canUseSlide({ height: 105, weight: 15, isWearing: function() { return false; } }); result.should.eql({ canUse: true, reason: '', }); }); it('should verify if you can use the slide: large adult instead of child', function() { var result = slide.canUseSlide({ height: 193, weight: 151, isWearing: function() { return false; } }); result.should.eql({ canUse: false, reason: 'This slide is for children only.', }); }); it('should verify if you can use the slide: too small child', function() { var result = slide.canUseSlide({ height: 83, weight: 11, isWearing: function() { return false; } }); result.should.eql({ canUse: false, reason: 'You are not tall enough yet.', }); }); it('should verify if you can use the slide: wearing shoes', function() { var result = slide.canUseSlide({ height: 105, weight: 15, isWearing: function() { return true; } }); result.should.eql({ canUse: false, reason: 'You must take off your shoes first!', }); }); it('should verify if you can use the slide: 2 children at the same time', function() { var result = slide.canUseSlide([{ height: 105, weight: 15, isWearing: function() { return false; } }, { height: 115, weight: 17, isWearing: function() { return false; } }]); result.should.eql({ canUse: false, reason: 'Only 1 person at a time!', }); }); });
5 possible situations, so 5 tests. But this test code hurts your eyes. So much copy-pasting going on here, agh. You can do better.
Let’s make an array of test situations. We’ll give each one a name (to put in the it description), the input of canUseSlide , and the expected output.
require('should'); var slide = require('./slide'); describe('canUseSlide 2', function() { var testCases = [ { name: 'child the slide is intended for', person: { height: 105, weight: 15, isWearing: function() { return false; } }, expectedResult: { canUse: true, reason: '', } }, { name: 'large adult instead of child', person: { height: 193, weight: 151, isWearing: function() { return false; } }, expectedResult: { canUse: false, reason: 'This slide is for children only.', } }, { name: 'too small child', person: { height: 83, weight: 11, isWearing: function() { return false; } }, expectedResult: { canUse: false, reason: 'You are not tall enough yet.', } }, { name: 'wearing shoes', person: { height: 105, weight: 15, isWearing: function() { return true; } }, expectedResult: { canUse: false, reason: 'You must take off your shoes first!', } }, { name: '2 children at the same time', person: [{ height: 105, weight: 15, isWearing: function() { return false; } }, { height: 115, weight: 17, isWearing: function() { return false; } }], expectedResult: { canUse: false, reason: 'Only 1 person at a time!', } }, ]; testCases.forEach(function(tc) { it('should verify if you can use the slide: ' + tc.name, function() { var result = slide.canUseSlide(tc.person); result.should.eql(tc.expectedResult); }); }); });
Excellent. You have separated the data from the test execution. It’s now easy and clear how to add another test. It’s even possible to put the test data in a different file.
However, your test file just went up from 68 lines to 78 lines. And there is still a lot of duplication.
In the test data, the expected result can be reduced to just the reason. During test execution, you can then create the expected result object from the reason (after all, canUse is true if the reason is empty, and false otherwise).
Also, you decide to make a helper function to create the person objects.
This is the final version of your test file:
require('should'); var slide = require('./slide'); describe('canUseSlide 3', function() { function createPerson(height, weight, isWearingShoes) { return { height: height, weight: weight, isWearing: function() { return isWearingShoes; } } }; var testCases = [ { name: 'child the slide is intended for', person: createPerson(105, 15, false), reason: '', }, { name: 'large adult instead of child', person: createPerson(193, 151, false), reason: 'This slide is for children only.', }, { name: 'too small child', person: createPerson(83, 11, false), reason: 'You are not tall enough yet.', }, { name: 'wearing shoes', person: createPerson(105, 15, true), reason: 'You must take off your shoes first!', }, { name: '2 children at the same time', person: [createPerson(105, 15, false), createPerson(115, 17, false)], reason: 'Only 1 person at a time!', }, ]; testCases.forEach(function(tc) { it('should verify if you can use the slide: ' + tc.name, function() { var result = slide.canUseSlide(tc.person); var expectedResult = { canUse: tc.reason.length === 0, reason: tc.reason, } result.should.eql(expectedResult); }); }); });
51 lines of lean and mean test code. The test data is very clear and compact. Not bad!
Your uncle installs your code on the PGS, and soon after, the MegaBanana Slide is fully operational. Everyone is happy!
Except that you still have this slight nagging feeling that it should be possible somehow to refactor the person field of the test data so you can move the createPerson calls from the test data to the test execution. But you can’t think of an elegant way…