Lesson 1 of 21

Introduction to Unit Testing Ideas

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

Markdown version

We learn what does "Unit" in "Unit Test (external link, opens in new tab)" mean, and what is the role of TDD (external link, opens in new tab) in software development. We also learn about the three types of unit tests we can write: Return Value Tests, State Tests and Interaction Tests (external link, opens in new tab).

In this lesson

Roy Osherove separates good unit testing (external link, opens in new tab) from test-driven development (external link, opens in new tab), which he traces to extreme programming and Kent Beck (external link, opens in new tab)'s book Test-Driven Development by Example and calls a personal practice. He defines a unit as a unit of work (external link, opens in new tab) that starts at a public API and ends in a return value, a noticeable state change (external link, opens in new tab) or a third-party call (external link, opens in new tab), the only place for mock objects (external link, opens in new tab). He argues for one test per end result (external link, opens in new tab) and questions claims that unit tests make code easier to debug, maintain and understand, as when 50 tests stop compiling after a constructor change.

Transcript

4,757 words

[00:03] (external link, opens in new tab) Welcome to the unit testing (external link, opens in new tab) and TDD (external link, opens in new tab) course. I want to talk about where test-driven development comes from because this course is has two parts. It has the part that teaches good unit testing practices and it has a part about test-driven development and those are two separate skills. You you can separate test-driven development from unit testing. In that you can write horrible tests in a test-driven way and you can write great tests after the code is written. So it's two separate skills of writing good tests and when to write them.

[00:37] (external link, opens in new tab) So but test driven development is the idea and became really official and g was given a name when people started talking about extreme programming. Has anyone heard of this term extreme programming? It's a little less known than scrum. But it's really where a lot of people from my I'm not going to say generation because I'm not old. But from from my background learned about TDD at the first time from a book by Kent Beck (external link, opens in new tab) about called test-driven development by example

[01:12] (external link, opens in new tab) And the and books about extreme programming and learning about this stuff and extreme programming is a is a very powerful methodology of working as developers. It's a very technical methodology. It's about what do you do on a day-to-day basis? How do you write the tests first? How do you work with user stories? And if we compare that to something like scrum, scrum is a very open-ended methodology or a process really that leaves a lot of holes in the m in the middle where you can put anything you want.

[01:45] (external link, opens in new tab) So scrum doesn't tell you to write tests. But I believe that if you want to do things like scrum and iterate very quickly and release quickly without unit tests it's almost impossible because the point of iterating quickly and getting feedback is that you're supposed to be able to do something within a week or two and it's almost impossible to do something over a week or two and release something when you don't have tests because fixing things on such a quick level is almost impossible because you don't know what you broke.

[02:20] (external link, opens in new tab) So half of the time will just go to fixing things and then you you'll have a hard time working forward in your features. So extreme programming is a much better technical methodology that you can use in scrum to make scrum work for example. But if we take test-driven development as a skill or as a practice, this is something you can take away and not use within the context of extreme programming.

[02:51] (external link, opens in new tab) You take you can take TDD and use it in any context in any methodology. So even if you work in a waterfall organization, test-driven development can still work because TDD or test-driven development is a practice between you and your computer. Just like debugging, it's something that you choose to do personally. So just like nobody tells you when to debug things, it should be just as equal that no one should tell you when to write a test first because it's just the way you work.

[03:22] (external link, opens in new tab) There is no question of whether you or not you should do it. If you choose to do it, it's the way you work. If you debug something three times and you go slowly over each line, that's your methodology of working and making sure that your code works. and TDD should feel almost the same, a personal choice. However, if your team wants to do TDD as a team and one person chooses not to do TDD, that's also a personal choice, but it's going to hurt the team. If your team chooses to start doing TDD, that's something that you can choose to do as a team as well.

[03:54] (external link, opens in new tab) And it's easier when you do it in pairs (external link, opens in new tab) because test-driven development at the beginning is very difficult because there's a lot to learn. The muscle memory that we usually do is to write the code first and then the tests. But it takes time to get used to a whole new way of working. Is it worth it? We'll talk about whether it's worth it either today or tomorrow. But we'll talk about numbers as well. And what we'll see is that it it can be worth it a lot because it can save you a lot of time

[04:29] (external link, opens in new tab) Until release. But the problem is that it's hard to see the numbers at the beginning. And so a lot of organizations fail to adopt things like TDD because they feel like they're wasting time and money, but they're not getting the output that they wanted. And the output can only be measured a few months or sometimes a year into the future. That's one of the reasons that a lot of organizations still do not do this today is because they haven't given it enough time to see the benefits. On the other hand, a lot of organizations do give time, but they do TDD and unit testing badly.

[05:03] (external link, opens in new tab) And so the time it takes is actually worse than we're not doing it. So test-driven development can actually hurt you sometimes. We'll talk about that. But to understand TDD, we have to understand unit testing. So I want to start by by trying to understand the idea of what is a unit in unit testing. Does anyone have any concept of what is a unit? What do you think it is? You're probably right.

[05:34] (external link, opens in new tab) >> Yeah. >> Well, from what what they said in some conferences, the common misunderstanding is that your unit is your a class something that you test all the aspects of a class. >> Okay. What they then told us is that unit is more of a well a functional unit sort of a a function a I'm not going to say user story but one aspect one one task that a user is supposed to do or one one task that the program is supposed to do. So unit is a task performed by a user for example or by an API.

[06:08] (external link, opens in new tab) That's that's the way I kind of think about it is that I used to feel that this is a method or a class or a bunch of methods. But today I start thinking of a unit as a unit of work (external link, opens in new tab). something that is a use case inside the system that has a public API as an entry point (external link, opens in new tab) where the test would test a public API and then something would happen and then one of three things is something that we end up with. So if we think of a unit of work, it's a public entry point that does something in the system.

[06:41] (external link, opens in new tab) For example, we can have return value return values. If we think about return values, that's the simplest form of a use case. You put something in, you get something out, right? You call add, you with one and two, you get back three. That's a very simple thing to test, right? You you test the the unit of work. Maybe you call the add method, but inside the add method, maybe add calls three different methods all in memory

[07:14] (external link, opens in new tab) And then it produces the return value directly. And that's the easiest type of test to write. But it is a unit of work because it does everything and I don't care what it does in the middle in the middle part. I just care about the return value. That's how I know it works. I could check that it calls internal methods inside, but that would actually make my test very brittle, very unmaintainable because I don't want to document how the internal code works.

[07:44] (external link, opens in new tab) I just want to document the external API and how it's supposed to be used. Now, and this is also important, the test is invoking a public API and it's asserting on a return value from the same API, from the same level of code. It does not assert on a return value lower in a lower level code. It doesn't assert on a return value from a private method. It always starts with a public method. Now, there's a reason that I like to always start with public methods.

[08:16] (external link, opens in new tab) Private methods tend to change. Also, private methods don't usually produce an end result (external link, opens in new tab). Private methods are part of a bigger process. If you think about it, private methods cannot exist without public methods. If a private method never gets called, what's the point of the private method? There has to be something that calls a private method. And if that something is private, something else has to call that something. But eventually it has to start with something public.

[08:47] (external link, opens in new tab) So there is any private method is part of a bigger use case that starts with a public method. So what we want is to find the use case that says okay there is value to a user by checking this code. There is no value by checking that a private method does something because a private method always operates in the context of something public. So I I would want to check the public thing. As an example, if I call add on a method,

[09:21] (external link, opens in new tab) But I check an internal field in the class that it's set to the right value and my assert passes, right? A private field sometime maybe I'll use reflection or something and I'll check that the private field contains the right sum and my test passes. Does it mean anything? It means that the private field has that value, but it doesn't mean that the code actually works because maybe

[09:51] (external link, opens in new tab) The methods that produce the result don't use that field. Maybe they use a different field by mistake. So my code might still not work even though my test passes. That's why I want to test things through the public APIs and also assert at the same level either through the same API or through a different one. So this is the simplest kind of test and it's called a return value test but it also can be thought of for exceptions because an exception is a form of return value at the same level of API.

[10:23] (external link, opens in new tab) So you put something in you get something back. That should be the simplest test (external link, opens in new tab). Those are not my cats. The second level of tests are when you have methods that don't return anything. So you invoke a method. Let's call the method add user. So if we save the meth the method add user doesn't return anything. So what's the point of the method? It changes the state of the system.

[10:55] (external link, opens in new tab) If we add the user to the system, the system now has a different state. Now again the question is if I add the user to the system what's the best way to test that as user works is it by going let's say it's an integration test (external link, opens in new tab) is by going to the database sorry log in with the new user that's a much better way why why do I think that's a better way than going to the database and checking the user there any ideas

[11:29] (external link, opens in new tab) >> you Yeah, it could it could be in the database and still not work. That's the point. The if the user doesn't end up being able to log in, it doesn't matter in how many tables something is in the database. The point is that there's supposed to be real value at the same level of API. So if I add the user, the login for the new user should work and then I don't care whether it's using a database or an XML file or an in-memory database. I should not care. and then I can refactor the code and my test should still pass. If I change the code and I check a database field, that's a more that's a more brittle test and it also might not prove that everything works.

[12:08] (external link, opens in new tab) That's why even if I'm doing integration tests and I'm doing something with a real database, I would still use the application level APIs to check that I can get something back assuming that it's in the database instead of quering the database because the fact that something is in the database doesn't mean that that something is actually being used by the application. So it has to be not just a state change but a noticeable state change something that the end user at the same level of API will notice a change in behavior.

[12:40] (external link, opens in new tab) Okay so that's the second level of test. We have return value and we have state change (external link, opens in new tab) or noticeable state change where you might invoke an API but then assert on a different API in the system. It could be that there is a public API that changes like a field that's public. That's also an example of something that's okay to test because it's a public field. It's a noticeable public field at the same level of the API.

[13:12] (external link, opens in new tab) But if it's private, then it's not noticeable. Okay, here's an example of state change. You turn off the light, you see a different system. See the difference? public state change. That's not my baby, by the way, just to make sure. So, a noticeable state change is by the user of the API at the same level and the act and the assert parts of the test should be at the same level of APIs.

[13:44] (external link, opens in new tab) Now, here's something a bit more interesting. if you invoke a method called add user but the requirement now is not just that it adds a user but also that it does something else like logging okay and logging happens on a web service somewhere does that mean how do I check that because this is a call to a third party (external link, opens in new tab) system now so you could have methods that don't return a value and do not have noticeable state changes in the system and those are methods that forward calls to other systems

[14:20] (external link, opens in new tab) And that's the third type of test third type of end result that's the most complicated one because you cannot assert on the third party system right if you let's say that you you want to have a system that sends an email to the user so you will invoke a public API that sends an email to the user and then how would you assert that the email got Would you go and and open an Exchange server somewhere and then literally go to the Exchange API?

[14:51] (external link, opens in new tab) You could do that, but that's a very difficult thing to do and it's going to be very very slow. It's going to be have a lot of configuration problems. So, it's going to be very hard to assert on the third party system. So, instead what we do is we create a mock object (external link, opens in new tab). We create something that looks like the third party system and we replace it in memory so that the third party call instead of going to the third party, it actually goes to our mock object and then we assert that the mock object got called

[15:21] (external link, opens in new tab) And that's the only place where mock objects make sense when we have calls to third parties. So of the three types of units of work of of end results. Okay. If we have three types, return values, state changes, and third party calls, the only place where mock objects make sense is third party calls. For the rest of the things, we don't need mock objects. We might need stubs (external link, opens in new tab). We might need to fake (external link, opens in new tab) some dependencies on the system, but we are not going to assert against those those dependencies.

[15:57] (external link, opens in new tab) The only place where we assert on something that looks like something else is in third party calls, and that's a mock object. And we'll talk about again mock objects tomorrow. But the idea is that we might use something fake in in the other places, but we're only going to assert either on state changes or return values. So the tests are going to be much easier. We're only going to assert on the fake things in the third party calls. And that shouldn't be more than five or 10% of your code. If your code has more than five or 10% in in tests with mock objects, I'm not talking about stubs, but mock objects that you assert against, then you're probably asserting too much.

[16:35] (external link, opens in new tab) You're probably oversp specifying your your tests and you're asserting on things that you shouldn't be asserting on. For example, you might have mock objects on third party calls, but also you might check state changes and also make sure that something else got called just in case. And that's an example of oversp specification. It's it should be much better to just check the state of the system because if if the end result is a state change, I don't care which other objects got called as long as the end result is a good state change.

[17:09] (external link, opens in new tab) If the end result is a third party call, I would assert on that. Now, we could have a method that both returns a value, changes the state of the system, and calls a third party at the same time. Do you agree? In fact, in enterprise applications, usually most methods do at least two things. They do something either state change or value and they do some logging, for example. So, you could have a method a unit of work that has two end results at the same time.

[17:42] (external link, opens in new tab) For each end result, you write a separate test. You don't write all those asserts in the same unit in the same test. Why? What's the point? >> Sorry, >> it's hard to change. >> It's hard to change the test. Not as much the the the big problem, but you're going to have a problem giving a good name for the test because you're testing multiple things. But even worse, for each one of the return value of of the end results at the bottom, we will have a a separate kind of assert.

[18:17] (external link, opens in new tab) One we will assert on the return value on the middle one we'll assert that something may be changed in the behavior of the system and on the third one we'll check on a mock object. What if we combine all of them and now we have three asserts in the same test and the first one fails. What's going to happen is we're going to get an exception in our test and the other two will never execute. So we won't actually know whether the other two end results actually work. That's another good reason to separate into multiple tests because when you have multiple tests, if one of the tests failed, you can still the other two still execute and so you know the state of the system.

[18:56] (external link, opens in new tab) So you're like a doctor who's looking at the system at the patient and the patient is either hiding information or not. So if the patient is only telling you a third of their symptoms, you're going to try to fix the problem in the wrong place. But if you see that this that value passed but the state failed a third party call got still passed it's much easier for you to fix. If you see that all of them fail it's much easier to know maybe where the problem is

[19:34] (external link, opens in new tab) Okay. This is an example of a a third particle or a mock object in real life. You know, you know what that is, by the way? That is what people do when they want to get your card information. So, they record it when you type and they actually forward it to the real system if they want to. Then they take it and they see what people clicked on. Now, so we know what a unit is. Now, let's see about popular myths about unit testing.

[20:11] (external link, opens in new tab) When people do these courses, they usually do them because they think that the following are true. It's easier to find bugs and maintain and understand your code when unit testing is happening or at least somebody told them that these are true, but they're not necessarily true. Is it easier to find bugs when you have a thousand unit tests? Not necessarily. Because the unit tests could have bugs as well, right? If your unit test has a bug,

[20:42] (external link, opens in new tab) How do you know? Would you write a test for your test? But how what if maybe the test for the test has a bug, etc. So, it's a very recursive problem. So, the only way I know to make sure that the tests do not have bugs is by test-driven development. Do you have something to say? For example, in TDD, you start with a failing test. So you get to see that the test fails when it should and then you move on and you make the test pass without touching the test only touching production code and suddenly the test passes.

[21:15] (external link, opens in new tab) So you know that the test fails when it should and it passes when it should. So you are essentially debugging your test when you do test-driven development. That's the only way I really know on testing the test. So if your test is is testing the wrong thing, it might pass when it should have failed at the beginning and then you know you got something wrong. Or if the test is wrong and it fails and then you fix the production code and the test still fails, you might say, "Okay, maybe there's something wrong with the test." But if I don't do these checks and my tests have bugs, it's possible that I'm not going to find the real bugs when they actually happen in my production code.

[21:54] (external link, opens in new tab) And then it takes longer and it's more frustrating because if if developers trust that the production code they write actually works with and they're wrong usually. Can you imagine what they think about their tests? The tests absolutely work right because they're usually simpler. But what if they don't? That's actually going to take much longer to figure out because it's the last place you will debug to know whether the bug is there or not.

[22:27] (external link, opens in new tab) So, it's not necessarily easier to find bugs unless you learn how to test your tests. It's easier to maintain your system when you have a thousand tests. What do you think? Yes. No, maybe. Perhaps. >> Depends. That's a good That's a good answer. Depends on what >> I would say. to make changes to the production code or the tests. >> It depends if you have to make changes to the production code or the tests.

[23:00] (external link, opens in new tab) Can you be a little more specific? >> You have to maintain your production. I would say it's easier to maintain because it's I would assume it's >> okay. So you would assume that maintaining the production code is easier >> than maintaining the test.

[23:31] (external link, opens in new tab) >> PD so it's easier to read. >> Okay. >> Most likely easier to understand. >> So I'm thinking on the other side if if something fails then you have to sort of maintain your tests that is that could not be so easy. Well, here's here's an example. What if we what if we have 50 tests for a class that we just created and in each of those tests, we create an instance of that class and then we do something with it and we assert and now we decide to add an argument to the constructor of the class.

[24:09] (external link, opens in new tab) What happens to those 50 tests? First of all, they stop compiling. So now we have to go and fix 50 non-compiling tests just because we added an argument to a constructor. So that sucks. Already our productivity was actually hurt by the tests. Not only did they stop compiling after we make them compile, they might still fail, right? Because they're sending maybe default values or wrong values. So now we have to go through 50 tests and fix them maybe or some of them.

[24:42] (external link, opens in new tab) So everything becomes horrible when you have a lot of tests if you don't do things in a maintainable (external link, opens in new tab) way. And one of the things we'll see today as well is how to make your tests a bit more maintainable so that if that situation happens we know what to do. For example, don't repeat yourself in unit tests is also a powerful rule that we can use to our advantage. So not creating classes directly in all the tests is something we'll try to do.

[25:13] (external link, opens in new tab) Now the third part that people usually say about unit testing is that it's easier to understand your code when you have let's say a thousand unit tests. And when they say that they actually mean that the test should be very readable in the form of almost documentation. And you can see a lot of that in things like RSpec and other forms of tests and specification. We're going to try to write write our tests. So that that's actually true. But most people, especially when they start with unit testing, the names that they give the tests are just bad.

[25:46] (external link, opens in new tab) They just say they don't they don't care about the names. When in fact, the name of the test is one of the most important things that you can actually do in your tests. Because if you give it a good name, first of all, it means you understand what is the problem you're trying to solve. And second, you're treating the reader of the tests as a first class participant in the code. And I have this simple rule that when I write my tests, I I imagine that the person who's going to read my code is a serial killer that knows where I live.

[26:19] (external link, opens in new tab) I don't want to piss them off too much. So, I don't want them to start asking unnecessary questions. For example, oh, what does that mean? or what is that variable and then they have to take an axe and take it to my house and knock on my door and ask me that question. So I try to protect myself from that by making things as obvious as possible in the test so that there should be almost no question that you cannot look at the code and say oh yeah I know that what that is without even switching to different methods in the test. It should be very very readable

[26:52] (external link, opens in new tab) And we'll focus on that as well in this course. So only when the first three things are true, then it is truly easier to develop your code with a thousand unit tests. But if you just take one of them and you make it false, the whole experience of developing your code with unit test becomes horrible and it's not worth it. And that's kind of the thing is we have to watch out for these things and then we actually get the benefit from unit tests. but it can easily derail into something that's very hard to maintain and very hard to read to the point where you start removing unit tests.

[27:27] (external link, opens in new tab) So in the first project that failed, I gave an example of some things that did not work for me. Now about six months into that project, we actually started removing some tests because the tests were hurting us. Every time we wanted to make a change to something, some tests were failing and we didn't have time for that stuff. we we actually had more important things to do. And we knew that the tests were failing for all the wrong reasons. They weren't failing because the code wasn't working. They were failing because they expected things to work internally

[28:01] (external link, opens in new tab) And we didn't actually care about. We kept changing it. Oh, that method should have been called three times and it was called four times. Well, the end result is the same. So, what do you care? Or that method is called in once instead of three because we optimized. Everything works faster. Result is still the same. Test is is failing. That's what you want to avoid.