Lesson 3 of 15

The Basics of RSpec

Duration: 40:40 Watch on YouTube (external link, opens in new tab)

Markdown version

Now that we have continuous testing set up, we will work on the String Calculator Kata (external link, opens in new tab) usiung RSpec.

In this lesson

Roy Osherove writes a failing spec for a bug in the calculator's second argument and removes the duplicated creation of the calculator, preferring a make calc factory method (external link, opens in new tab) to a before block (external link, opens in new tab) or let. He gives the three parts of test-driven development (external link, opens in new tab): no line of code without a failing test, solve it as simply as possible but no simpler, then refactor. He starts the string calculator kata (external link, opens in new tab) with an empty string and then a single number, making as many assumptions as possible and removing them one spec at a time, and commits to git whenever the tests pass.

Transcript

4,694 words

[00:05] (external link, opens in new tab) Okay. Just a note is that you will also have to install guard RSpec. So that guard will actually work. Forgot to mention that. So you'll have to gem install guard RSpec as you can see right here. Okay. Now let's do guard minus C to have that running. So let's see what happens when we start adding things to our to our calculator.

[00:36] (external link, opens in new tab) Okay, for example we have a bug that we're only checking the the first argument, right? We're only checking X. If we look at my calc, we're only checking X. So let's say that we this is our program and we found the bug in our program and we want to check x and we want to check y. So one option is to go and add a check an if here or another return and then add a test.

[01:12] (external link, opens in new tab) But if we actually found a bug, why not write a test first? Prove that we know that the bug exists bug exists and then see the test pass. that would be a much better way. So in production if you if you find a bug the best way is to try to recreate it with a test. That way you can easily see when you fixed it. So what if we say over a thousand but we want to have the same test

[01:45] (external link, opens in new tab) For the second argument. Let's say return zero checking the second argument. Now it's absolutely the same test but we want to say one and a,000 one like this. If I save it now what happens? Let's look at this zero checking the second argument.

[02:16] (external link, opens in new tab) So I can see now in the list is that I have this checking the second argument expected zero but was a 102. Okay now already I'm not liking what's happening here. A lot of duplication in my tests. We'll fix that in a second. First we have to get the tests passing. So what if I just do the simplest thing that could possibly work (external link, opens in new tab)?

[02:46] (external link, opens in new tab) Return zero if y is see what happens now. Now they both pass. Okay. So what's wrong with this test? It looks exactly like the other and that's kind of the problem. And here we get into the idea of maintainability (external link, opens in new tab). If we talked before about the fact that if you have many tests, it can actually hurt maintainability.

[03:20] (external link, opens in new tab) This is where the problem starts. We have two tests calling my calc new. If I change the calculator initializer tomorrow to require arguments, I'm going to have two tests that I have to fix. Two tests is not so bad, but it means 20 tests tomorrow. So the idea is that I fix and I refactor the tests as soon as I start to see duplication but not before. So I get one test working, it's okay. If I get two tests working and they look the same, then it's not okay.

[03:50] (external link, opens in new tab) So I don't prefactor, just refactor when I find duplication. So here's something that that's possible for us to do. We can try to use a before article here. So you might say before do and we can say C equals my C. We can put it as the

[04:23] (external link, opens in new tab) Calculator. We can say calc Yeah. And I can get rid of this line. And I still get the same result on the right.

[04:54] (external link, opens in new tab) But now I have a single place where if if my if my initializer changes, I only have to fix a single place. That's pretty nice, right? But there are other ways to achieve this maybe without a before. Now the problem with a before structure, first of all, I'll just explain what it is because I haven't explained it yet. A before structure h is an is a is like a setup method (external link, opens in new tab) in xunit.

[05:27] (external link, opens in new tab) It's a piece of code that gets executed once for each of the tests, each of the specs. So if I say puts in setup and I save it, we should see in setup twice on the right here and here. And that gives us the ability to initialize a new calculator for each new test. But it also decouples the it from the test in a way that way if I just read the specs itself,

[06:02] (external link, opens in new tab) It's very hard to understand where calc came from. It's not hard now when there's only a single test. It can be very difficult when I have a lot. So one thing I like to do instead is to is to have a factory method (external link, opens in new tab). Okay. Now a factory method can be a a method that looks like this

[06:37] (external link, opens in new tab) Def make calc and it just returns my calc. still works the same the difference is that I can see where calculator is being created where I'm getting it from

[07:10] (external link, opens in new tab) Okay I don't have an instance I'm calling the maker directly the amount of code is basically the Okay. Now there are other ways to do this. There's another way called let. And let is basically a way to define an argument that says calc I forgot the status of I almost never use it.

[07:42] (external link, opens in new tab) Basically let's see if we say RSpec let

[08:16] (external link, opens in new tab) Okay. And the difference is that now I can actually go and use it. So I would just say just use it as if it's a method. I can go and say calc like this. Okay. So this line is using the calc that was defined here. Okay. Now my problem with that is that there's too much magic still. Although I can look at the context and say look at this context there is a calc like this

[08:46] (external link, opens in new tab) And this gets invoked. It's almost like a setup method. It's basically a simple way to define a setup method for a specific object. There are two risks with this. First I go to somewhere down the line and I have to figure out where call came from. So I have to look for the lets and the second risk is that I can have multiple let object objects that are only used in some of the of the its on some of the specs.

[09:19] (external link, opens in new tab) So and this is the same problem that exists in XUnit frameworks (external link, opens in new tab) is that in setup methods you can initialize objects that are only used in some of the tests in the current class. So it's important to have those lets defined in specific area in under a specific tree subree etc. And I find that that's very much almost a hindrance to productivity in reading the tests. So I like to be much more specific and just say

[09:50] (external link, opens in new tab) Call make calc and be done with it. Now the nice thing about make calc I can just define it at the describe method if I want to. Still works and I can have the context nice and simple. I can also have a context that says for example over a thousand. I can have a context that says add.

[10:20] (external link, opens in new tab) I can have a context that creates more instances and then have some people like the its to have to be only a single row so that the act part is defined in the hierarchy above here. So it could be that we can actually do it like this. Oops, sorry. It should be like a result and then you would have result like this.

[10:50] (external link, opens in new tab) Oh, now I forgot what to do. Oh, you have to do it in a special block. And that's the exactly the type of thing that I'm I don't like too much is that now you have to remember how to define it specifically with a specific tree. And I don't like it. I like to keep things very very simple. So I just use the context as hierarchy to define like the reporting mechanism and then I use this.

[11:22] (external link, opens in new tab) So I like to use factory methods instead of lets instead of setups instead of befores and I find that this makes the test much simpler to read and much simpler to understand. Questions. >> There's no difference in scope between what? >> Yeah.

[11:54] (external link, opens in new tab) >> as long if it's in the root scope, it's available from the from the other ones. So it doesn't really matter. >> Okay. >> No, not across files. It's only nested in the describe block. But when we get to more complicated stuff, we can have we can just have require for different files that have the the specification for making calculators. And there are even more complicated stuff. So we can have frameworks like Factory Girl to to initialize objects for integration testing if we want to.

[12:27] (external link, opens in new tab) But remember keep things simple. Okay. What I want you to look at is this actually this one we're going to talk about test driven development (external link, opens in new tab) now because we actually did some test driven development just now. We started with a test it failed and then we make we made it pass.

[12:57] (external link, opens in new tab) We and then we did refactoring but to the tests not to the production code. So what I want to do is to talk about TDD and then we'll use a string calculator kata (external link, opens in new tab) to learn about testdriven development. So when we talk about TDD there are three parts to it. The first part is no line of code without a failing

[13:28] (external link, opens in new tab) Test. That means that if you if you look at your code, your code is basically read only when all your tests are passing. If you want to change your code, you have to write the failing test first. The second rule is when you solve it solve it as simply as possible but no simpler.

[14:01] (external link, opens in new tab) Okay. What does it mean to solve it? I can say make the test pass but solve it as quickly as possible. Now, as simply as possible, I'll show you what I mean, but it basically means that don't over organize and overarchitect for the future unless you absolutely know that the future is certain. And even then, don't do it. Only do things that allow you to move in in the future to more easy easy things.

[14:33] (external link, opens in new tab) But try to have as many assumptions as possible about how things are being sent to your to your code. And don't worry about your code being less generic because if you plan for the future too much, you end up having to pay a big price up front for things that might not be true. So amount of users that will be in the application, amount of code or API that you need that will be used only do the simplest thing could possibly work.

[15:07] (external link, opens in new tab) And the third part is refactoring. Now refactoring is making the code readable and maintainable because specifically you made it as simply as possible. So simple as possible might end up pretty ugly and then you refactor it to be more maintainable and understandable. Okay. So that's why I didn't care about going to my calc and having two return that look exactly the same because I'm more interested in getting the functionality done than anything else.

[15:42] (external link, opens in new tab) And once it works then I can refactor. And the nice thing is that I can refactor and I can know if I broke something because now I have tests. So it actually makes me more productive. I will end up with good code, but it will actually work and I will know when I break it. So the cutter we're going to look at now, let's see, is to is is at a.com/kata.

[16:17] (external link, opens in new tab) Okay, so I want you to go there now. And the kata is that of a string calculator. And this this the the situation is very simple. Imagine that you are running a bank related calculator and every night the bank receives a file from each related bank about the customer 34.

[16:48] (external link, opens in new tab) Okay. And in the file you might have num you just have numbers. You might have a number you might have file two that looks like this. Okay. And your job is to basically sum up the string in the number and return the result and then later it gets saved into a database or and changed in the deposit account etc. Okay. So this is basically a batch result file from some from customers that made deposits to different accounts. Okay.

[17:19] (external link, opens in new tab) And our job is to create a string calculator that can So now what I'm going to do is I'm going to create a spec for the string calculator. String calc spec.

[17:49] (external link, opens in new tab) And I'm going to start with the describe string calculator. And I save that. And I immediately get this nice response. It says, "Dude, uninitialized constant string calculator doesn't exist." But I'm not worried about that yet because I want to get the first test passing or first test failing. Okay. Okay. So, as far as I'm concerned, this is almost like a compilation problem than anything else.

[18:20] (external link, opens in new tab) So, now I can go and say, okay, let's describe adding and I want to describe the simplest scenario written here in the kata. So, this is the first rule. The method can take zero, one or two numbers and will return their sum. For empty string, it will return zero.

[18:50] (external link, opens in new tab) For example, are the are the only possible inputs, but you can have 10 or 20 here. You can have a thousand, but it can only be zero run or two numbers. Now, the trick about this kata, if you never done it before, is to treat it like a a 50-day long project. So that every day you only know about one thing you have to build and you don't read ahead about the future things.

[19:20] (external link, opens in new tab) I know that once I say don't read ahead you immediately want to read ahead. I know I know but if you treat it like this it will help you to focus on not planning too much for the future doing the simplest thing that could possibly work. Okay. So the first feature is to add the ability to sum one zero one or two numbers. Okay. So what should we test for?

[19:53] (external link, opens in new tab) What should we test for the first thing? What would you like to test under what input? >> Sorry. >> Yeah. But how many what which numbers do you want to send in? What input do you want to send in? Sorry. >> Start with a blank. >> Start with a blank with an empty. >> Why? Why not one number send in? Why not two numbers?

[20:27] (external link, opens in new tab) No particular reason. You should have a really good reason. And the reason is that you want to start with the simplest test and the simplest requirement. So if I wanted a parser, right, I need to parse the numbers, right? If I need something that takes a string of numbers because it's a string and a string of numbers has either empty or one number or two numbers, definitely empty is the easiest. So I'm going to start with an empty. So adding empty string,

[21:01] (external link, opens in new tab) It returns zero. Okay. Now I I'm not even going to say describe string calculator. I'm going to say describe string calculator like this. It says, okay, return zero. But I don't have any asserts. So the test isn't failing.

[21:31] (external link, opens in new tab) I don't get to write code yet. But I don't have a failing test. So what should I write in the failing test in the failing spec? Well, let's first create the string calculator. And now the point is that I'm writing test-driven development. So I'm writing the code for the specs as if the code that I wish I had already works. But not only that, because the code doesn't exist yet, I get to imagine how the code looks as if it was perfect.

[22:02] (external link, opens in new tab) That's the nice thing because now I can say, you know what, I'd love to be able to do to do this and I would type it up and then it would actually come into existence. Isn't that nice? So then I can say, I wish I could do this. String calculator new add an empty string.

[22:33] (external link, opens in new tab) Now I can just say result should equal zero or should be zero. Bam. Now it fails. Now I get to go to my code, go to lib, and add a new string. Sorry. String calc and create a class

[23:07] (external link, opens in new tab) String calculator. Let's see what happens when I save it. It runs a spec, but it says name error uninitialize constant string calculator. What the hell? string calculator. Oh, I don't have a a using statement the require. So I can say require

[23:44] (external link, opens in new tab) What happened? Oh, require relative twice. That's good. Apply relative. Then hit the slip. Now it says undefined method add. Okay, don't worry about doing it now. Just look undefined method add. You're going to have time to do it later. undefined method add.

[24:15] (external link, opens in new tab) So I need to add the def method. Def. Let's see what happens now. Now it says argument error wrong number of arguments. Okay. So we definitely forgot to add numbers. Now it says okay expected zero got null. Okay. What's the simplest code I could possibly write to make all the specs pass? Return

[24:50] (external link, opens in new tab) Zero. Return zero. Return zero. Really? You'd be happy with me returning zero. Would you be happy with that code? No. Okay, let's do it anyway. >> Return zero. Oh, actually passive. That's good. So, we're done, right? Yay. Actually, we made a lot of assumptions, right? We made the assumption that numbers is always going to be zero, right? The assump that number is always going to be empty. And that's actually pretty good because now we're not happy with the code.

[25:24] (external link, opens in new tab) Unfortunately, we cannot change it. It's read only. All the tests are passing. We want to change it. We're not happy with it. What do we have to do? We have to write another failing spec. Right? So if we are not happy with it, it we're actually forcing oursel to write more specs on in the test-driven process. One of the ways to drive into more specs that cover everything is especially by making as many assumptions as possible while writing the code. And that forces us to then to remove the assumptions one by one with new specs.

[26:05] (external link, opens in new tab) So if I go here and I say you know what no it can't always be empty that's that's not good code then I can go and says I can go and says sure I can go and say but there's another context context single number do and a single number it returns that number

[26:36] (external link, opens in new tab) Right now. I'm going to do exactly what I'm did before. I'm going to paste it. And I'm not happy that I pasted it. But once this test passes, I can refactor the tests to make them more maintainable. And I run the spec. Oh, should be one and one. Now, why did I choose the number one? because it's the first single number that I can think of. So I don't I won't confuse the reader the serial killer with asking him myself him asking me why did you write 42 right because that's usually what people write 42 or something

[27:12] (external link, opens in new tab) So okay I just proved an assumption wrong that the string is not always empty so if I go to add I can now have the chance to make add better by removing that assumption So now I can just say return zero if numbers is empty. Okay, but then what what do we return if it's not empty?

[27:49] (external link, opens in new tab) >> I can parse a string. That's right. What's the simplest code I could write to make all the specs fast? >> Return run. Really? You'd be happy with that? You're crazy. Return run. Let's do Let's do it anyway. We just made everything pass. Not good. Not good. This is not leading in the quality way that I promised you. We are just derailing. Okay. So now we have two problems, but we have two good problems.

[28:19] (external link, opens in new tab) First, our problem is that we're returning one, but we're already answering one feature. And the second problem is that our tests are not maintainable. So, what we want to do is to extract something into a method. So, I can extract this into a method. Extract method make out. Where is that method? There it is.

[28:52] (external link, opens in new tab) Let's put it right there. Oh, maybe we want to put it maybe there. Probably missing something. What am I missing? Make calc add. What's it saying? formal argument cannot have a constant.

[29:22] (external link, opens in new tab) Oh, that's right. What? There you go. Now I can later put the make in a separate file if I want to. But now I can just go ahead and say okay we got rid of some of the maintainability issues in the test. But you can see now here

[29:55] (external link, opens in new tab) String calculator adding single number returns that number empty string returns zero. Okay. What about this code? That's not good. Can we do anything about it? >> No. What do we have to do? >> We have to write it a spec that proves that we cannot always assume that a string that's not empty is always one.

[30:30] (external link, opens in new tab) So, we have to prove that. So it's in the context of a single number, right? Or I can prove it in a different context. I can prove it in the context of multiple numbers. I can prove it is that it's a different single number. Which one should we choose? >> Sorry, >> single number. >> Single number is simpler because we haven't solved two numbers yet. So it's simpler to solve something that we already solved once. So it returns that number

[31:03] (external link, opens in new tab) Even if it is not one, even if it is different. for different numbers. Let's see what happens now. And I just moved it to two. Why not 42 again? Because I don't want to confuse the reader.

[31:34] (external link, opens in new tab) So I say, okay, for two it doesn't work. Now I get to go here and say, okay, it can't always be one. I realize that. Can I say if it's one, then return one. If it's two, then return two. I could say that, but that would not be the simplest way. That would be the longest way. Okay? So simple doesn't have to be stupid. So once I see I have this repetition I can just go ahead and say just return numbers to i.

[32:06] (external link, opens in new tab) Now it passes. Okay. I can go and I can try to refactor the code now. For example, I can say we can refactor this into a method that says empty input because empty input today is is strings but tomorrow it could be

[32:38] (external link, opens in new tab) Ns as well. Let's do that.

[33:16] (external link, opens in new tab) That's an example of getting rid of a specific thing. So what I want to do is to have this method only know one thing. Maybe there the algorithm of how to parse numbers. But I don't want it to know how it converts a number to a string or a string to a number. I don't want to know what is the default number. for example, I can turn this into a const for example. Okay, so I want this method to only know about the algorithm and then to use small methods so that I only have to change one thing if I have to change a specific concern.

[33:48] (external link, opens in new tab) So I might say return factor extract method and call it oh this okay I still have don't like the refactoring thing in Vim.

[34:18] (external link, opens in new tab) So I might have to do this return. What is that? Just the default value default value Now you see I just broke something. It shouldn't be the default value.

[34:49] (external link, opens in new tab) It should be should be convert to number should be this because I'm so excited.

[35:24] (external link, opens in new tab) Okay, back to safety. And now, do you guys use git? This is the point. Every test that passes and every test that that I'm feeling happy, I will just get and add things to the git repo. So, I would add a lot of commits during this. I would all probably have like 10 by now. So if I actually go here and I say git init add git commit working.

[36:00] (external link, opens in new tab) So every time I would do some one of refactoring and test passes I would go and do a quick git commit. And the reason is is that if I go down a rabbit hole, it's very easy for me to come out later. Just get delet I can actually do undos from git as well. More questions. So what I want you to do now is to go through the string calculator and to check and to basically try and do as much of it as possible. I just want to teach one more thing about the specs.

[36:32] (external link, opens in new tab) If I want one of the requirements is to make the string calculator throw an error. If you get a negative number at some point, if I wanted to check that it gives an error, then I can say

[37:03] (external link, opens in new tab) That it raises an error when something I can say to raise error.

[37:35] (external link, opens in new tab) That should be just about what you need. So that would be the line. Okay. And then you would get expected exception but nothing was raised. Okay. Okay. So what we're going to do now is we're going to work on the string calculator refactoring and going through the entire ka okay for the rest of the day. Hopefully we'll finish it today. But I can tell you that some teams very very few teams finish it on the first day.

[38:10] (external link, opens in new tab) So don't worry if you feel slow because it's supposed to it's supposed to be a bit difficult. And I want to tell you one last thing is that the best learning comes from when you are actually feeling very uncomfortable. The best learning happens when you feel like you have no idea what you're doing. When you are feeling like you're failing. If you feel like you're stuck, etc. It's okay to feel stuck, but you should also feel good because it means you're in the process of learning something. Okay? So, have fun not having fun.

[38:43] (external link, opens in new tab) Okay? Be comfortable feeling uncomfortable, etc., etc., etc. Good luck. May God help you. Now, I want you to take a look at this structure. I know I said that setup methods and lets were pretty bad, but what happens when that's true when you don't have a lot of tests. Now, when you do have a lot of tests, one thing that I found that actually does help is that you use let like factory methods. So, here I put the let really at the top of string calculator.

[39:18] (external link, opens in new tab) Okay. Now, what I like about this is that I can actually have the same code as a factory method like this. I can still have the same maintainability aspect that I had before. It makes the test more readable. And if I need string calculators that looks different, I can go ahead and and say let's say calc with bad state a string calculator new with

[39:49] (external link, opens in new tab) Something bad happening to it and then I can use the variables as if there were methods really. Okay. So, you can actually do that and it's it's a when you do get a lot of specs in there, it does clean up the code quite a bit. So, at the beginning, you can actually use factory methods, but at some point, if you get a really long kind of a file, if you put the lets at the top of the file and not nested in multiple places,

[40:25] (external link, opens in new tab) That can be still be readable. If you put them in multiple places, it could be very very unreadable to look at all the places where where they reside. Okay. So you can do you can try and do that if you want to.