Lesson 2 of 14
Test Driving a String Calculator
Roy Test Drives the creation of a string calculator (external link, opens in new tab) with explanations on best practices.
In this lesson
Roy Osherove introduces code katas (external link, opens in new tab) for practicing TDD (external link, opens in new tab) and starts the string calculator kata (external link, opens in new tab), whose Add method returns the sum of the comma-separated numbers in a string. He writes one test at a time, starting with an empty string returning zero, and makes each pass in the simplest way (external link, opens in new tab), from always returning zero to int.Parse, finding a bug on the way. He runs the tests with NCover, saying coverage (external link, opens in new tab) means little without test reviews (external link, opens in new tab), and sends the audience to do the kata in pairs (external link, opens in new tab).
Transcript
[00:04] (external link, opens in new tab) Okay. So with that I want to introduce to you the idea of a code kata (external link, opens in new tab). Who here has heard of this katas for code? One person that's that's a good start. Code katas are exercises that you can do in code. But this kata is special for TDD (external link, opens in new tab) purposes. If you want to learn how to do TDD, most of it is about muscle memory. Okay, it's really easy to learn the theory, but if you want to learn TDD for real, you have to exercise with it.
[00:37] (external link, opens in new tab) You have to do it. One of the katas that I would recommend is a simple one called a string calculator (external link, opens in new tab). If you go to osherove.com you might see this description over here and the idea of the kata is to create a simple calculator class.
[01:07] (external link, opens in new tab) It has one method that only takes one parameter and it's a string. That string can contain zero or more numbers divided with a comma and it returns the sum of all the numbers that it finds. That's the whole idea. As you progress through the the exercises, it gets more and more complicated. So more and more demands on how on what needs to happen, more features requested. You should think about this as a project that every day you get a new feature that you never thought of.
[01:39] (external link, opens in new tab) So try not to read ahead if you ever do the kata. We will later we'll do take a 30 minutes and try to do it ourselves as pairs so you can feel what it's like. It's a good way to practice TDD because it's not very complicated. It's more about learning through your fingers how to do something. In the page itself, you can see a lot of videos on how to on how other people exercise the kata in basically all of the recent languages.
[02:09] (external link, opens in new tab) So, Java, Ruby, C, Erlang, Scheme, Haskell, couple of them in Perl and PHP. So, and there is one in JavaScript actually. So, you can see how it's done in JavaScript as well. And what I'm going to do is I'm going to show you the first steps of the kata right now so you can see the TDD process although you already know some of it and after that I'm going to you're going to try it yourselves.
[02:39] (external link, opens in new tab) So I'm going to assume that every two people here will have at least one laptop because the exercise is done in pairs (external link, opens in new tab). So the first thing we do when we start the kata is we start with a blank solution. Now, if I if this was my three or five day TDD course, I would actually tell people disconnect your mouse from your machines and try to do the whole thing without a mouse just for the added difficulty just to make it a bit more fun. But for now, don't worry about it. So,
[03:10] (external link, opens in new tab) If I create a new solution, oh, that's not a new website. I just want a new solution. And I choose other project types. Empty solution, blank solution. I call it kata. And in the kata, I add a new project class library. We're going to call it
[03:41] (external link, opens in new tab) Kata.ests because we don't have any integration tests (external link, opens in new tab) here. Now because it's TDD we start with test first. So the first requirement in the kata is create a simple string calculator with this method. It can return zero or more it can return zero for an empty string or the sum of numbers if there are any numbers in the string.
[04:14] (external link, opens in new tab) So I'm starting with the tests. So, first thing I'm going to do is I'm going to rename this into string calculator tests because that's the name of my class. I'm also going to set the package manager. So, I'm going to so I I'm going to save time later by investing a little time right now. Package going to look for package manager console and I'm going to give it a shortcut alt n
[04:46] (external link, opens in new tab) It's not taken I'm just going to assign it from now on whenever I need to install something I just press alt n install package NUnit (external link, opens in new tab) so that will save me some of the work later now I have all the references I'm going to remove two of these And because I don't need them and
[05:20] (external link, opens in new tab) This one. No. >> Oh, I'm in English. That's why. >> Okay. Test fixture. Start with a test. The first test and this is sometimes we face this dilemma. We could write a lot of different tests for this method. So which one should we write first? So the answer is if we want to work in an incremental fashion, which is usually what we want to do, we start with the simplest test (external link, opens in new tab) that will drive us to create something in the simplest way.
[05:53] (external link, opens in new tab) So if we have a set of demands such as it can return zero for an empty string, it can return a number for a single number, it can send it two numbers or 15 numbers. Which one of them is the simplest requirement to implement? What do you think? >> Empty string. >> Empty string. It just feels simpler, right? Sometimes you don't know but it feels like this is the smallest thing.
[06:24] (external link, opens in new tab) It it's also incrementally the beginning of a feature. You can start with numbers but you might forget to do an empty string later. So we can start with add empty string because add is the name of the method and the expected behavior is returns zero. So for added returns zero for an empty string. So now we can create the class.
[06:55] (external link, opens in new tab) Remember the class in this case doesn't exist yet, not just the method. So I might say string calculator which doesn't exist yet. And I'm not going to make it compile until I finish the whole test. SC equals new string calculator. I'm not going to refactor this because I don't have two tests that use the constructor yet. The point here with TDD is to drive new functionality. So I don't worry about refactoring the test yet until I have two passing tests.
[07:27] (external link, opens in new tab) SC.add empty string. Okay. Introduce a variable int result and assert. Now there are two types of syntaxes for nunit which I only showed you one of them. You can do assert r equal but there's also assert that result
[07:58] (external link, opens in new tab) Is and then we have multiple things equal to zero. So some people like this syntax better. It's your choice as long as you're consistent. I personally like the first one but a lot of people like this one. To me, this is less readable than just assert or equal. But it's just a choice. You should know that this exists. So, I'm going to do assert. You know what? Let's just keep it this way.
[08:30] (external link, opens in new tab) So, now if I'm happy with the test, I'm going to make it compile and I'm going to see it fail. To make it compile, I'm not even going to create the separate product project. I'm just going to create it right here beneath my test. Create a class. Create a method. Now I the default of the of the variable names are not good. So I'm just going to change them to be more readable numbers. Should I keep this or not?
[09:01] (external link, opens in new tab) What do you think? >> Except exception or what? >> Yeah, throw new not implemented exception. Some people like to keep this so they can see the test fail. I personally like to see the test fail on something that feels like the real system. So, I'm actually going to return a bad value on purpose. Return minus one. It just feels better to me. I don't have a good explanation why this makes more sense.
[09:32] (external link, opens in new tab) Both of these will work, but this is closer to the solution that we need. This feels more like a bug that I would want to fix. Maybe that's my my explanation. So now that I have this, I can go and for just once right click and run the test in this project. And of course it fails. And that's awesome.
[10:02] (external link, opens in new tab) And now I get to fix it in the simplest way possible. Remember laziest developer in the world has to go home in two minutes. What do they do? They just make as many assumptions as possible. The assumption I can make here is I'm always going to get an empty string because no one can prove otherwise yet. So I can just always return zero. And again, this is something that feels not intuitive, but the practice of simplicity is something that at the beginning you need to learn how to do to simplify your actions.
[10:34] (external link, opens in new tab) Right now all this does is it it proves to us that we're missing more tests because we're not happy with this code. So what we're going to do since this code is not read only, you have to write another test. So I'm going to write another test and this time it's still the add method. The scenario is going to be something that will drive me to remove this assumption that it's always empty. Okay. So what scenario should I choose?
[11:06] (external link, opens in new tab) What do you think? >> send in one. >> The number one or one number >> one string. >> One in the string. Okay. Just the number one. >> Yeah. Which number? >> One. Just one number. That is one.
[11:41] (external link, opens in new tab) Never mind. Okay. U the scenario. So how would you explain this? A single number. Okay. Any number, right? A single number. A single number is simpler to prove than two numbers. So add with a single number. What should it do? >> Returns that number. Okay. So now I can say string calculator and I can already feel that I would need to refactor but only after this test passes.
[12:13] (external link, opens in new tab) The point is to drive functionality that I can actually demo. Refactoring comes later. SC equals new string calculator. SCA add what is the number one? one number of one, right? Result int results assert or equal.
[12:45] (external link, opens in new tab) We should expect. What should we expect? You sound very quiet. Assert that we get back one. from results. Run all the tests. One passed, one failed. We can see the results here. Expected one but was zero. I just need to look at the test name (external link, opens in new tab) to know.
[13:15] (external link, opens in new tab) Okay, now we get to make everything right. The simplest thing possible to make all the tests pass. What do you think I should do now? >> Return one. return one. If I return one, it will break the first test. So, I can't do that. >> Make all the tests pass in the simplest way possible. >> Return numbers. >> Return numbers, but it's a string. It's supposed to be an int. It's supposed to return an int. >> Parse.
[13:46] (external link, opens in new tab) >> Parse Make sense? int.Parse will turn a string into a number. Okay? Okay, let's run all the tests. Weird. That's weird. Could we actually have a bug in one line of code? Okay. Ed empty string return zero failed.
[14:19] (external link, opens in new tab) Input string was not in the correct format, dude. Okay, so I guess we just discovered our first bug. So that was simple, but it doesn't work. So what do we have to do? What should we do? What do you think? Obviously, it means that int.Parse doesn't work with empty strings. Okay, so we need to make the simplest thing but still pass all the tests. So that doesn't work. >> Try parse. Try parse is an if it's the same thing.
[14:50] (external link, opens in new tab) Try parse most people don't know it so I'm going to use a a different way if numbers length is zero let's return zero okay otherwise what should I do you know you know what I'm going to do right >> parse it >> parse it return one. >> Can I make an assumption here?
[15:22] (external link, opens in new tab) >> Return one. >> I can make an assumption. If it's not anti- string, it's always one. >> Horrible. Horrible. Two passed. Unfortunately, I can't change this now. >> So, if you wanted me to fix this, we're going to have to have another test. But before we do that, let's make the test a bit more maintainable (external link, opens in new tab). So, I'm going to extract this into a factory method (external link, opens in new tab). Method make calc.
[15:53] (external link, opens in new tab) Same thing here. Run all the tests again to make sure I didn't break anything. That's why I want all the tests to be passing. But if I change the test and something breaks, I know that it's because of my change. If I refactor the test while something is breaking, I won't know it's it's breaking because of my changes or it's breaking because it always breaks. So, I want to always have a clean sheet. a a sheet of knowledge. Okay. So we have concluded this is not great.
[16:27] (external link, opens in new tab) I'm assuming that you're not happy with this code. So what we're going to do is we're going to prove that we cannot make an assumption that the number is always one. To do that we can just go here and turn this into a test case. So for example I'm going to make this into a parameter alt p. This is the numbers. Control Alt P. This is the expected. Go to test case. Send in one. Expect one.
[16:58] (external link, opens in new tab) Just run all the tests again. Okay, two passed. Duplicate with control D. What number should I send in? >> Two. >> Two. Perfect. Run all the tests. Gotcha. Okay. expected two but was one. Okay, I guess I can't hide anymore with my with my assumptions that it's always one. So, what should I do? What's the simplest way to make all the tests pass?
[17:36] (external link, opens in new tab) Someone once suggested you just do an if. If it's a one, you return one. If it's a two, you return two. But that's not as simple as just int.Parse. it's not as simple. So what's the difference? Simple is shorter. Simple is less indentation. If I need to do an if, I have more indentation, right? So visually, I can actually say that something is simpler than something else. If it's more to the left than the other thing, for example, if it's closer to the beginning of the method, it's simpler than if it's the bottom of the method. But in this case, this might be everything I need in parse.
[18:09] (external link, opens in new tab) Sometimes the the actual solution is a solution. Okay, but it took us three tries to get here. So, parse numbers, run all the tests. Three pass. Now, several interesting things here. First of all, if my manager comes in and says, got a demo for me, I can actually tell them, look, it's partially working. It's working for an empty string and it's working for single numbers.
[18:41] (external link, opens in new tab) It's actually partially working. It's incremental developments. It doesn't work with multiple numbers yet, but it will. So if I can say that something works now 10 minutes after, it means that if I chose to release today, I would have a partial release. If I'm working in iterations of two weeks then and I'm working in micro iterations like this of every few minutes it means that every two weeks the the release day is just another day because every day every hour we have something we had that we didn't have an hour before
[19:12] (external link, opens in new tab) Because it always working. That's a big difference from working on something for three days and it's not working at all. Then suddenly it works right and you're always in the 95% working. It's almost working like 95%. Just another 90% to go. Okay, that so you should never feel like that again. If you work in an incremental way, you always have something to to return. The simplicity goes toward that feeling.
[19:43] (external link, opens in new tab) Okay, so I guess that's working. But you know what? Maybe I can make another assumption that I didn't consider. For example, and this is one thing that sometimes I force myself to do, although I don't expect you to remember to do it on your own. Sometimes it works to remember it, sometimes it doesn't. I can make an assumption about the length of the number if I wanted to. So, I can say numbers are always one character, right? So, I can parse the numbers just the first character.
[20:15] (external link, opens in new tab) I don't care about anything else, the string. all the tests will still pass. Although I added code to make this happen. However, if I do this, it forces me to write another test that says numbers can be more than one character. And if I don't do this, I don't even consider writing that test. A lot of times we don't think about tests that seem obvious. But for now, this just a thinking point. Here's another thing you can do.
[20:46] (external link, opens in new tab) Test with NCover. So NCover is a free tool. This is an older version. It's a free tool that tells you the coverage (external link, opens in new tab) details of something. Right now we have 100% code coverage. Okay, in our tests and in our code. If you work in the in the that incremental way in the simplest way possible then every piece of code that you do has a reason to exist because a test expected it.
[21:16] (external link, opens in new tab) So that every piece of code is covered. So you should expect 95 to 100% code coverage if you do the things this way. Having said that high code coverage doesn't mean anything. Okay? If I tell you that I have 100% code coverage or if you tell me we have 100% code coverage, that doesn't mean anything to me because maybe all your tests don't even have asserts. Okay, so code coverage doesn't mean that you have quality. It just mean that you have code coverage.
[21:47] (external link, opens in new tab) However, low low code coverage, 20% code coverage means something. What does it mean? We have 20% code coverage. What does it mean? >> We're missing tests. That's all it means. 100% doesn't mean anything. It only means something if you do test reviews (external link, opens in new tab). If you look at each other's tests and you review them to make sure that the tests are good. If I know that your tests are good, then 100% means something.
[22:18] (external link, opens in new tab) But if I don't know that your tests are good, then 100% is meaningless. So as a goal, if you ever had a goal to strive for coverage, it should be to strive for coverage plus test reviews. Okay? Because when you only have the goal of coverage, then as developers, we are only aimed at just having a test that does something. We're not aimed at quality. If we have test coverage, then the test will be good and the coverage will mean something.
[22:48] (external link, opens in new tab) So I can go on with this. For example, I can say, okay, what if I give you more than a single number because right now you assume that you can parse the number. But if I have two numbers, I'm pretty sure you can't parse that because there's a comma in between them. So add with multiple numbers. Why didn't I just add it to the previous one? Why do you think? Think about that for a second. We'll talk about that in a second as well.
[23:20] (external link, opens in new tab) Multiple numbers The difference is in the scenario. Okay. So if someone reads multiple test cases, the serial set and they see one, two, and then suddenly like weird stuff, there is no explanation why that weird stuff exists in those parameters. The name of the test should explain generically everything
[23:50] (external link, opens in new tab) That the test cases above it do. If it doesn't, for example, it says single number, but my I have some inputs with multiple numbers. It doesn't make sense. So either I change the scenario, but I don't want to change it because this is a specific feature to handle a single number. It's a different feature to handle multiple numbers because I'm working incrementally. So now I can say make calc exact variable and that should be sc
[24:21] (external link, opens in new tab) Add and again the simplest numbers would be one and two in results are equal we're expecting three from result run all the tests this time it's going to run with coverage because that's the last action that I You can see 86%. Because something failed. Okay, I'm going to run them without coverage. Now
[24:52] (external link, opens in new tab) It says input string was not in correct format just for multiple numbers. So now I have to do the simplest thing that could possibly work and make all the tests pass. Any ideas? Split the string. >> Split the string. >> Yeah. Okay. So, but then will it know how to handle a single number if I split the string?
[25:24] (external link, opens in new tab) >> Okay. So, here's what I want you guys to do. Split into pairs. Two people on one machine. You switch. Every time one person writes a test, the other person solves the test. Okay? One person is lazy, the other is not. And then you switch the next time. Try to do the kata on your own. Get all the way to this situation. Try to solve it on your own in the simplest way possible. Okay? If you're working in JavaScript, do it in JavaScript.
[25:54] (external link, opens in new tab) There's a there's a framework called Jasmine, which I recommend. I I'm not a JavaScript developer yet so I'm not going to be able to help you but setting up the environment might be a good learning opportunity for you anyway. Okay. If you're doing in .NET do the things that I did in .NET. Any questions before we get started. >> Okay. So let's take 30 minutes and see where we get in 30 minutes and I'll just run around and look at how you write code and give you various tips and tricks.
[26:27] (external link, opens in new tab) Okay. >> Good luck. Let's be careful out there.
[27:13] (external link, opens in new tab) >> So, if you don't remember the URL, It's