Borrowed heavily from Laracasts.com. Go give Jeffrey money and learn a ton!
To learn Test-driven Development (TDD) with simple examples.
The key to learning successfully is to follow TDD strictly.
- Start by writing a failing test case. Do not write any production code without a failing test case.
- Write only enough production code to make the error change or the tests pass.
- When your tests are green, you can refactor.
- Once you are pleased with your production code, move on to writing the next failing test.
I'm not advocating that this should always be your approach when developing. What I am advocating is that following this pattern is a great way to learn testing and refactoring.
You will need CommandBox in all of these katas. Click here to download CommandBox. Choose the correct version for your platform and follow the instructions on the site to install it on your computer.
- Create a new directory on your computer called
code-katas. cdin to the new directory.- Open up the folder in your editor of choice.
- Run CommandBox by typing
boxin your terminal or command prompt. - Run
install testbox --save. This will install TestBox in your directory root. - Run
server start port=9999to start up a development server. It should open up automatically in your browser. - Browse to
http://127.0.0.1:9999/testbox/test-runner/to see the TestBox test runner. - Type
testsin theBundle(s) or Directory Mappingtext field and clickRun. You should see your first test in the window below.
Create a roman numeral calculator that successfully converts decimal values from 1 to 5000.
Click here for a quick reference sheet.
- Create a
RomanNumeralCalculatordirectory andcdinto it. - Create a
srcdirectory and atestsdirectory. - Create your first test by running
testbox create bdd tests.RomanNumeralCalculatorTest - Make sure your server is running and navigate to
http://127.0.0.1:9999/testbox/test-runner/to see the TestBox test runner. - Type
roman-numeral-calculator/testsin theBundle(s) or Directory Mappingtext field and clickRun. You should see your first test in the window below. - Open up the new test (
RomanNumeralCalculatorTest.cfc) in your code editor.
Let's take a look at our first test case. Glance at the following code:
function run(){
describe('Roman Numeral Calculator', function() {
it('calculates the roman numeral for 1', function() {
// Your tests go here.
});
});
}Your tests go inside the run function. Inside there you can group your tests in many ways. We're going to stick to describe and it.
describe groups similar tests together. There's a lot of benefit to this, especially with code reuse, but we're going to ignore that for now. There has to be at least one describe block in your tests, though, so let's create one and name it Roman Numeral Calculator.
Your tests go inside describe blocks. You create a test by using the it function. You pass in two parameters: the spec name and a function. Inside the function is where you will write the code for your test.
We want to use descriptive names for our specs so our tests can act like documentation as well. There's no space limitations here, so don't worry about abbreviations or line wrapping or whatever! Just make sure it would make sense to someone else what the test is doing.
We also want to make sure our tests are only testing one thing at a time. Name your tests in a way that forces you to do this.
Again, this is an ideal world. Sometimes you are testing multiple things at once. But right now, we're practicing TDD, so only one thing at a time!
If you refresh your test runner, you can see your spec in the window. Now it's time to write the test.
One of the benefits of TDD is that it helps you think about how you want the public API for the component to act. Instead of rushing to our code editor and writing a component and an API we will regret later, we can use our tests to write out how we wish we could interact with the component and then go make it happen.
Let's fill in our first test case:
it('calculates the roman numeral for 1', function() {
var converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
var actual = converter.convert(1);
var expected = 'I';
expect(actual).toBe(expected);
});Here we are instantiating our converter component, calling a method named convert passing in our decimal value, and expecting that our actual value equals our expected value which is I.
Expectations are how we test things in TestBox. We expect a value to have certain characteristics. The basic ones would be
toBe,toBeEmpty,toBeGT, etc. Each of the Expectations have a correspondingnotExpectation (notToBe,notToBeEmpty,notToBeGT, etc.) The full list can be found here.
Let's run our test!
Looks like TestBox is reporting an error:
invalid component definition, can't find component [RomanNumeralCalculator.src.RomanNumeralConverter]
Believe it or not, this is a good thing! Our test is telling us we haven't yet created our RomanNumeralConverter component. Let's do that now.
// RomanNumeralConverter.cfc
component {
}That's it. Don't write anything else. We only want to do the bare minimum to get the error to change or the test to pass. Let's go back to our test runner and run this test again.
We changed the error! Congratulations! This is exactly what we want to have happen. Let's look at the next error.
component [RomanNumeralCalculator.src.RomanNumeralConverter] has no function with name [convert]
Again, our failing tests are helping us design our component. In this case, it is failing because we have not yet created a convert method on the component. Let's add one and re-run our test.
New error: variable [ACTUAL] doesn't exist.
This is a little more criptic error. Our tests don't always give us the exact next step. Let's think about it. actual doesn't exist because we don't return anything from our convert method. Well, if we return I from our convert method and re-run our test....
It passes! We're so good!
Now you might be thinking here, "Hang on....That's not going to work for any other example." You are right. It won't. But it passes all of our current tests. If there are more requirements this component must meet, we need to write another test first. This is one of the mantras of TDD.
Again, you might think this is stupid and a waste of time. Sometimes it is. But it can also be interesting to follow this approach and see how it leads your design. At least in these kata exercises, just follow TDD. 🙂
At this point, your RomanNumeralConverter.cfc should look something like this:
component {
function convert() {
return 'I';
}
}Before we can write any more code, we need to write another test. Let's test that it can convert 2 in to II.
it('calculates the roman numeral for 2', function() {
var converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
var actual = converter.convert(2);
var expected = 'II';
expect(actual).toBe(expected);
});Re-run your tests, and we have another failing test case.
Now, what is the simplest way we can make all these tests pass? To me, it's an if statement.
function convert(num) {
if (num == 1) {
return 'I';
}
else {
return 'II';
}
}Re-run our tests and they all pass!
Once again, you may be thinking "This test is horrible!" You are right, and it will get better as we write more tests. Stick with me.
Let's test that our converter can convert 3 in to III.
it('calculates the roman numeral for 3', function() {
var converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
var actual = converter.convert(3);
var expected = 'III';
expect(actual).toBe(expected);
});Why not just add another if statement? (We'll fix this in the next section, don't worry.)
function convert(num) {
if (num == 1) {
return 'I';
}
else if (num == 2) {
return 'II';
}
else {
return 'III';
}
}Re-run our tests — they all pass!
Okay, now that our tests are all green, we can spend some time refactoring, both in our production code and in our tests.
First, let's look at the production code. You might have noticed a pattern. We are returning a number of I's equal to the number passed in to the function. Modeling that could make our code a good deal simpler. Let's go refactor.
function convert(num) {
var romanNumeral = '';
for (var i = 1; i <= num; i++) {
romanNumeral &= 'I';
}
return romanNumeral;
}Re-run our tests — still passing! A successful refactor. Isn't that great? Having tests helped us refactor with confidence.
Let's also refactor our tests and introduce you to another feature of TestBox. You might have noticed that we are creating our converter in each it block:
describe('Roman Numeral Calculator', function() {
it('calculates the roman numeral for 1', function() {
var converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
var actual = converter.convert(1);
var expected = 'I';
expect(actual).toBe(expected);
});
it('calculates the roman numeral for 2', function() {
var converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
var actual = converter.convert(2);
var expected = 'II';
expect(actual).toBe(expected);
});
it('calculates the roman numeral for 3', function() {
var converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
var actual = converter.convert(3);
var expected = 'III';
expect(actual).toBe(expected);
});
});We can extract this into one of the TestBox lifecycle methods: beforeEach.
beforeEach does what it sounds like: it runs the code before each it block in the current block (and any children blocks). Let's stick the instantiation of the component in that block.
describe('Roman Numeral Calculator', function() {
beforeEach(function() {
this.converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
});
it('calculates the roman numeral for 1', function() {
var actual = this.converter.convert(1);
var expected = 'I';
expect(actual).toBe(expected);
});
it('calculates the roman numeral for 2', function() {
var actual = this.converter.convert(2);
var expected = 'II';
expect(actual).toBe(expected);
});
it('calculates the roman numeral for 3', function() {
var actual = this.converter.convert(3);
var expected = 'III';
expect(actual).toBe(expected);
});
});Run our tests — still passing! Great job.
I have to make a confession — I misled you a bit with suggesting the beforeEach. I did it because I wanted to introduce you to beforeEach, but there's a better lifecycle method for our purposes: beforeAll.
You probably saw beforeAll at the top of your generated test file. It runs once before any of the tests run. Since our converter doesn't have any state, we don't need a new one each time. We can save these extra instantiations by moving the initial instantiation to the beforeAll method.
component extends="testbox.system.BaseSpec"{
/*************************** LIFE CYCLE Methods ***************************/
// executes before all suites+specs in the run() method
function beforeAll() {
this.converter = new RomanNumeralCalculator.src.RomanNumeralConverter();
}
// executes after all suites+specs in the run() method
function afterAll(){
}
/******************************* BDD SUITES *******************************/
function run(){
describe('Roman Numeral Calculator', function() {
it('calculates the roman numeral for 1', function() {
var actual = this.converter.convert(1);
var expected = 'I';
expect(actual).toBe(expected);
});
it('calculates the roman numeral for 2', function() {
var actual = this.converter.convert(2);
var expected = 'II';
expect(actual).toBe(expected);
});
it('calculates the roman numeral for 3', function() {
var actual = this.converter.convert(3);
var expected = 'III';
expect(actual).toBe(expected);
});
});
}
}Re-run — still green. Did it matter whether we used beforeEach or beforeAll? Probably not. But that's one of the fun things with TDD! It's a playground for trying out new ideas.
That's an intro to TDD here, but there's a lot more to this project. Keep working at it until you have a successful Roman Numeral Calculator!