“You are here to learn the subtle science and exact art of code-crafting. As there is little foolish wand-waving here, many of you will hardly believe this is magic." --Professor Snape
Hogwarts has embraced Muggle Technology!
Professor Arthur Weasley has just discovered the magic-powered computer, Hex, and it works at Hogwarts.
Young Wizard, you are creating Hogwart's online student registration. Professor Neville Longbottom will guide you.
Because you are a highly disciplined Wizard, you will write your code test first.
You have git installed.
git clone https://github.com/zhon/react-hogwarts-tdd-kata.git
cd react-hogwarts-tdd-kata``
npm install
npm start
Visit localhost:3000/index.html
to see the code running.
To run the tests:
npm test
or
npm run test-watch
How will you begin, my young wizard friend?
2. I notice tests running in one console
.
2. I notice the server running in another console
.
- I load http://localhost:3000/index.html.
- I notice the three menu items.
- I've sorted myself into a house by clicking on the sorting hat.
Great, what house are you in? I am in ___________________
Acceptance: Students are able to see a catalog of courses.
It is time to start coding. Where will you start? Making changes to catalog UI inside file src/components/catalog.js
.
Can you remind me how view to view the catalog? Oh, Professor, just run npm start
and hit webpage localhost:3000/index.html
and click on the Catalog menu.
What should the catalog look like? It has a table with headers, class, profesor, cred...
Can you show me in a test? Oh, that is easier.
If you copy paste the code remember, there is context and content; copy only the content. Yes, Professor and the next time through, I will type all the code.
test/unit/components/catalog.spec.js
describe('Catalog component', () => {
it('renders html headers', () => {
const catalog = TestUtils.renderIntoDocument(
<Catalog />
);
const titles = TestUtils.scryRenderedDOMComponentsWithTag(catalog, 'th');
expect(titles[0].getDOMNode().textContent).to.equal('Class');
expect(titles[1].getDOMNode().textContent).to.equal('Professor');
expect(titles[2].getDOMNode().textContent).to.equal('Credits');
expect(titles[3].getDOMNode().textContent).to.equal('Time');
});
I see you expect a Catalog
component with headers. Yes, we show "Class", "Professor", "Credits" and "Time".
src/components/catalog.js
render() {
return (
<table className="table">
<thead>
<tr>
<th>Class</th>
<th>Professor</th>
<th>Credits</th>
<th>Time</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
);
}
I reloaded localhost:3000/index.html
and clicked on menu item catalog
and I don't see the list of courses. It is because we haven't hooked them up.
Also, professor, webpack makes it so you don't have to refresh the web page.
I see only headers. How will we add a course? By adding a Course
component inside the <tbody>
tags and passing course
as a property.
Can you show me your test? Sure.
Demonstratio Facilius.
test/unit/components/catalog.spec.js
it('renders a course', () => {
const catalog = [ {
id: 'RUN105',
name: 'Ancient Runes',
startTime: new Date(0, 0, 0, 11, 30),
professor: 'Bathsheba Babbling',
credits: 3,
} ];
const renderedCatalog = TestUtils.renderIntoDocument(
<Catalog catalog={catalog}/>
);
const courses = TestUtils.scryRenderedDOMComponentsWithTag(renderedCatalog, 'td');
expect(courses[0].getDOMNode().textContent).to.equal('Ancient Runes');
});
How are you making it pass? I added a single course using the (soon to be written) Course
component.
Why not add all the courses now? I am taking small steps to keep my thinking clear.
src/components/catalog.js
render() {
let catalog = this.props.catalog || [];
let course = (catalog.length > 0) ? catalog[0] : null;
...
<tbody>
<Course course={course} />
Everything is broken! Nothing shows in the browser and the tests are failing. Yes, it is because Course
is not defined. I am defining it in src/components/course.js
src/components/course.js
import React from 'react';
export default class Course extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return null;
}
}
Notice I remembered to import course into catalog.js
.
src/components/catalog.js
import Course from './course';
How come you didn't add tests when you added Course
. We are working on a broken test. I will add tests later; but first, I will see this one pass.
Facere Transeat
src/components/course.js
render() {
let course = this.props.course;
if (!course) {
return null;
}
return (
<tr>
<td>{course.name}</td>
<td>{course.professor}</td>
<td>{course.credits}</td>
<td>{course.startTime}</td>
</tr>
);
}
You have a perfectly good catalog! Thank you.
I looked at the catalog web page and I didn't see any classes? It is because Catalog
is a stateless component (it only uses props
). We need a place to manage state
.
What is the name of the state managing component? I like CatalogPage
?
It turns out we have a CatalogPage
. Now what? I am changing CatalogPage.render
to pass catalog as a property.
src/components/catalog-page.js
...
<div className='panel panel-default' >
<Catalog catalog={this.state.catalog}/>
How and where did this.state.catalog
get the catalog data? this.state.catalog
is being set in the constructor
from the CatalogRepository
.
Why are we only seeing the first course? Remember, we are keeping things simple. I am still working on the test 'render a course'.
You have a test in the wrong place? Yes, I have a Course
test mixed in with the Catalog
tests. I am moving that now.
I am removing
test/unit/components/catalog.spec.js
it('renders a course', () => {
...
});
and placing it into course.spec
with a few modifications
test/unit/components/course.spec.js
import React from 'react/addons';
import {expect} from 'chai';
import Course from '../../../src/components/course';
const TestUtils = React.addons.TestUtils;
describe('course component', () => {
let course = {
id: 'RUN105',
name: 'Ancient Runes',
startTime: new Date(0, 0, 0, 11, 30),
professor: 'Bathsheba Babbling',
credits: 3,
};
it('renders a course', () => {
const renderedComponent = TestUtils.renderIntoDocument(
<table>
<tbody>
<Course course={course}/>
</tbody>
</table>
);
const courses = TestUtils.scryRenderedDOMComponentsWithTag(renderedComponent, 'td');
expect(courses[0].getDOMNode().textContent).to.equal('Ancient Runes');
});
});
What about ensuring Catalog
contains a Course
? I will add that test soon. ;-)
Now, can I see all the courses? Yes, I am adding them now; because, my code is clean and my test pass and are in the right place.
test/unit/components/catalog.spec.js
import Course from '../../../src/components/course';
...
it('renders all courses', () => {
const catalog = [ {
id: 'RUN105',
name: 'Ancient Runes',
startTime: new Date(0, 0, 0, 13),
professor: 'Bathsheba Babbling',
credits: 3,
}, {
id: 'AST101',
name: 'Astronomy',
startTime: new Date(0, 0, 0, 11),
professor: 'Aurora Sinistra',
credits: 3,
}, {
id: 'DDA302-10',
name: 'Defence Against the Dark Arts',
startTime: new Date(0, 0, 0, 10),
professor: 'Severus Snape',
credits: 4,
},
];
const renderedCatalog = TestUtils.renderIntoDocument(
<Catalog catalog={catalog}/>
);
const courses = TestUtils.scryRenderedComponentsWithType(renderedCatalog, Course);
expect(courses).to.have.length(3);
});
How did you get it to pass?
I removed this code
src/components/catalog.js
let course = (catalog.length > 0) ? catalog[0] : null;
and added this code
src/components/catalog.js
<tbody>
{ catalog.map( item => <Course course={item} key={item.id} /> ) }
Very nice work! I see courses. Thank you!
The course start time is not showing; I hope that doesn't mean you will be tardy to my class. No Professor Longbottom, now is the the right time to fix it.
test/unit/components/course.spec.js
it('renders time correctly', () => {
const renderedCourse = TestUtils.renderIntoDocument(
<table>
<tbody>
<Course course={course}/>
</tbody>
</table>
);
const data = TestUtils.scryRenderedDOMComponentsWithTag(renderedCourse, 'td');
expect(data[3].getDOMNode().textContent).to.equal('11:30 am');
});
I am adding moment.js
and replacing <td>{course.startTime}</td>
src/components/course.js
import moment from 'moment';
...
<td>{course.credits}</td>
<td>{moment(course.startTime).format('h:mm a')}</td>
Are we finished with the story? No, Professor Longbottom. Before calling a story done, it must be tested and deployed.
But this is only a Kata, we will start on the real registration system week when you have a pair. Ok, I won't deploy it and I won't write automated acceptance tests. But I must inspect my beautiful work (and make sure it is working).
Can you remind me how to see your work?
npm start
http://localhost:3000/index.html
click on Catalog (at the top).
Well done, young Wizard. You have finished your story. Another point for Hufflepuff. Thank you, I like to 1) write the test, 2) see it fail, 3) write code to make it pass, and then 4) refactor. I also like seeing what the end user sees.
Acceptance: Students register from the course catalog then view their courses on the schedule page.
You have shown how to build tested components that display data from a store. I would like to see some interaction. Sure, how about we add a register link to the course listing?
That works. Where will you start? I will add a register
link to Course
.
Don't you mean add a test? Yes, Professor; this is a TDD Kata, after all.
test/unit/components/course.spec.js
it('renders a register link', () => {
const renderedCourse = TestUtils.renderIntoDocument(
<table>
<tbody>
<Course course={course} />
</tbody>
</table>
);
const data = TestUtils.scryRenderedDOMComponentsWithTag(renderedCourse, 'td');
expect(data.length).to.equal(5);
expect(data[4].getDOMNode().textContent).to.equal('Register');
expect(data[4].props.children.type).equal('a');
});
Now I make the test pass.
Addo Tabula Registeriom
src/components/course.js
<td>{moment(course.startTime).format('h:mm a')}</td>
<td><a href="#">Register</a></td>
Excellent. I see the "Register" link on the page now. But it doesn't do the right thing when I click on it. No Professor, we are about to add an onClick
event.
What should happen when the link is clicked? It calls an action which registers the course to the wizard using the site.
Correct. There is already a wizard-actions
file for that. But how will you test that the correct action is called? I will use a mocking spell.
Mockus expectramis
test/unit/components/course.spec.js
import sinon from 'sinon';
...
import WizardActions from '../../../src/actions/wizard-actions';
...
it('calls WizardActions.registerForCourse when the register link is clicked', () => {
const mockWizardActions = sinon.mock(WizardActions);
mockWizardActions.expects('registerForCourse').once().withExactArgs(course);
const renderedCourse = TestUtils.renderIntoDocument(
<table>
<tbody>
<Course course={course} />
</tbody>
</table>
);
const data = TestUtils.scryRenderedDOMComponentsWithTag(renderedCourse, 'a');
TestUtils.Simulate.click(data[0]);
mockWizardActions.verify();
});
Very good. I see you are using the sinon
library to create a mock version of the wizard actions.
src/components/course.js
import WizardActions from '../actions/wizard-actions';
...
<td><a href="#" onClick={this.handleRegisterClick.bind(this)}>Register</a></td>
...
handleRegisterClick(event) {
event.preventDefault();
WizardActions.registerForCourse(this.props.course);
}
Good work. Now when we click the register link we should see the course in the schedule webpage. Hey! I get an error on the page that says, "Wizard pure-blood requirements not met." That's discrimination! Sure, only one of my parents was a wizard, but--
Yes, yes, you're right. Calm down; that error shouldn't be there. We'd better check the src/actions/wizard-actions
code. I see that error message in the registerForCourse
method. But it's hard to understand what's going on in that code.
Indeed. Malf-- that is, the wizard who worked on this project didn't write very clean code. Let's see if there are any tests. I see some tests in test/unit/actions/wizard-actions.spec.js
What does the registerForCourse test tell you? It looks like when you register for a course, it updated the wizard repository and then dispatch two events: registerForCourseSuccess
and updateWizard
That sounds correct. Are there any tests for the error message behavior? No.
Since we have a passing test, we should refactor the wizard-actions
file so we can figure out what's going on.
Can you figure out what any of those variables are for? Yes. The c
variable passed into the function must be the course we are registering for. And the w
variable is assigned to the wizard data we get back from the repository. I will rename them to make it easier to understand the code.
Nomer changus
src/actions/wizard-actions.js
registerForCourse(course) {
const chk = (x) => { return x.house; };
const advi = 4;
const wizard = WizardRepository.get();
const h = chk(wizard);
const adv = 'h';
// Check for mudbloods.
if (h[2] !== 'y') {
return this.actions.registerForCourseFailed('Wizard pure-blood requirements not met.');
}
wizard.courses.push(course);
if (h[advi] === adv) {
// DO NOT REMOVE!
course.credits++;
}
WizardRepository.save(wizard);
this.actions.registerForCourseSuccess(course);
this.actions.updateWizard(wizard);
}
Good. How about the h
variable? What is it for? Well, h
is assigned to the result of chk(wizard)
which returns the wizard's house name. So I can cast the rename variable spell again.
Nomer changus
src/actions/wizard-actions.js
registerForCourse(course) {
const chk = (x) => { return x.house; };
const advi = 4;
const wizard = WizardRepository.get();
const house = chk(wizard);
const adv = 'h';
// Check for mudbloods.
if (house[2] !== 'y') {
return this.actions.registerForCourseFailed('Wizard pure-blood requirements not met.');
}
wizard.courses.push(course);
if (house[advi] === adv) {
// DO NOT REMOVE!
course.credits++;
}
WizardRepository.save(wizard);
this.actions.registerForCourseSuccess(course);
this.actions.updateWizard(wizard);
}
I see your tests are still green. Very good. This means you haven't broken anything while making the code easier to read. Thank you. But I can't figure out what advi
or adv
are for.
Those two mystery variables only appear to be used once each. Since the names aren't helpful, you can try replacing the variables with the values. Yes, I see. We can do something similar with the chk
function. We don't need a separate variable to track the wizard's house.
Variate inlinus
src/actions/wizard-actions.js
registerForCourse(course) {
const wizard = WizardRepository.get();
// Check for mudbloods.
if (wizard.house[2] !== 'y') {
return this.actions.registerForCourseFailed('Wizard pure-blood requirements not met.');
}
wizard.courses.push(course);
if (wizard.house[4] === 'h') {
// DO NOT REMOVE!
course.credits++;
}
WizardRepository.save(wizard);
this.actions.registerForCourseSuccess(course);
this.actions.updateWizard(wizard);
}
Now we're getting somewhere. It appears that Dra-- I mean, the other developer was looking at specific characters in the name of the houses. Very crafty. Instead of checking for "Slytherin", he is just looking for the "y" at index 2.
Gryffindor also has a "y" at index 2, you know. But Hufflepuff and Ravenclaw do not.
There is not supposed to be any kind of pure-blood requirement for registering for courses at Hogwarts. I'll just delete those lines.
Wait. It's always better to use a test to expose a bug. That's something to put into your Remembrall. Okay, I'm changing the existing test. The specific house shouldn't matter, so I am removing Slytherin
.
Adapto Testatio
test/unit/actions/wizard-actions.spec.js
it('invokes registerForCourseSuccess and updateWizard on success', () => {
const wizard = {house: '', courses: []};
Very good. Now you can fix the code. And I'll get rid of that offensive "mudblood" comment too.
src/actions/wizard-actions.js
registerForCourse(course) {
const wizard = WizardRepository.get();
wizard.courses.push(course);
if (wizard.house[4] === 'h') {
// DO NOT REMOVE!
course.credits++;
}
WizardRepository.save(wizard);
this.actions.registerForCourseSuccess(course);
this.actions.updateWizard(wizard);
}
Now when I click on the register link on the webpage, I get a success message! And the course shows up on the schedule page too.
Good. But you aren't done here. To keep code maintainable, you should follow the "Wizard Scout Rule" and leave the place cleaner than you found it. Ah yes. There is still that bit of code that is checking the house and adding additional credits. But it says "DO NOT REMOVE" in all capitals. It sounds important.
Comments can be misleading. There is no requirement for giving certain houses more credit for a course. Very well, I will expose the bug and fix it.
Are you sure you want to write a test for a case that shouldn't exist? Good point. I am removing that code then.
src/actions/wizard-actions.js
registerForCourse(course) {
const wizard = WizardRepository.get();
wizard.courses.push(course);
WizardRepository.save(wizard);
this.actions.registerForCourseSuccess(course);
this.actions.updateWizard(wizard);
}
Mutating data is a common source of bugs. React
is so powerful because it encourages/requires immutability. Oh, and the code is mutating wizard
. I have just the spell:
Expeto Immutate
src/actions/wizard-actions.js
registerForCourse(course) {
const wizard = WizardRepository.get();
const newWizard = React.addons.update(
wizard, { courses: { $push: [ course ] } }
);
WizardRepository.save(newWizard);
this.actions.registerForCourseSuccess(course);
this.actions.updateWizard(newWizard);
}
Clicking on the register link now results in a success message and the course appears on the schedule page. Are we finished with this story? It depends, should we disallow scheduling more than one course at the same time (unless they have a Time-Turner)?
Yes, but that is another story. Then the software works as expected. The code is clean. Yes, I would say this story is done.
Congratulations, two points for Hufflepuff. Now, as soon as I get this Leg-Locker Curse off, we can go to the Quidditch match.
Acceptance: Clicking multiple times will result in all houses being selected.
We have a disaster! Sorting Hat is celebrating at Hogsmeade with Nymphadora Tonks' ghost and refuses to come home. The replacement, the old straw thing that sorted you, is sorting everything according to this Kata! I am not sure I see the problem.
Everyone is being sorted into Hufflepuff! Oh, no!, I could have been in Gryffindor! What can we do?
We must change the Kata immediately to sort randomly. I am on it.
How will you find the bug? I could open the debugger and...
You have tests, why not use them to help locate the bug? I am not sure how.
The sorting hat code has to be located in a store
, an action
or a component
. Only script kiddies would put it in a component. Most likely it is in the actions. Yes, I found it in WizardActions
, but how will writing test help avoid the debugger?
Are there tests for WizardActions
? Yes, but none of them check which house the wizard was sorted into.
Missing tests are common bug locations. Sometime, you might have a test file but the test is missing. Code coverage tools can help you find missing tests. Good to know.
You now have a choice, write a test or open the debugger. I choose test (this is a TDD Kata after all).
test/unit/actions/wizard-actions.spec.js
describe('sortIntoHouse', () => {
...
describe('when generating a random number', () => {
let stubMath;
beforeEach( () => {
stubMath = sinon.stub(Math, 'random');
});
afterEach( () => {
stubMath.restore();
mockWizardRepository.verify();
});
it('saves Gryffindor for random range 0.0 - 0.249', () => {
stubMath.returns(0.249);
mockWizardRepository.expects('save').once().withArgs(sinon.match({house: 'Gryffindor'}));
WizardActions.sortIntoHouse();
});
it('saves Slytherin for random range 0.25 - 0.49', () => {
stubMath.returns(0.49);
mockWizardRepository.expects('save').once().withArgs(sinon.match({house: 'Slytherin'}));
WizardActions.sortIntoHouse();
});
it('saves Ravenclaw for random range 0.5 - 0.749', () => {
stubMath.returns(0.749);
mockWizardRepository.expects('save').once().withArgs(sinon.match({house: 'Ravenclaw'}));
WizardActions.sortIntoHouse();
});
it('saves Hufflepuff for random range 0.75 - 1', () => {
stubMath.returns(0.99);
mockWizardRepository.expects('save').once().withArgs(sinon.match({house: 'Hufflepuff'}));
WizardActions.sortIntoHouse();
});
});
Nice work with the test coverage. Thank you, Professor.
I made it pass by replacing the randomize section with the correct algorithm (straight from Arithmancy class).
src/actions/wizard-actions.js
...
let randomize = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Have you looked at the website? Yes students are now being sorted into different houses.
Excellent! Three points for Hufflepuff.
The Kata is officially over and Stinksap's not poisonous. If you are here with working code, you are awarded an Acceptable OWL. If you want a NEWT or a higher grade, complete all of the following stories/tasks.
As you can guess, the Schedule page is using the Catalog
component.
Acceptance: Students attempting to register for multiple classes at the same time will be shown a message saying this is not allowed and the second class will not be added to their schedule.
Acceptance: Students with a time-turner are allowed to register for multiple classes at the same time.