Lesson 9 of 21
Demo - Mock Objects
A short demo showing how you can implement the extract interface pattern (external link, opens in new tab), constructor injection (external link, opens in new tab) and hand written mocks (external link, opens in new tab) to solve our problem of controlling a 3rd party logger in a unit test (external link, opens in new tab).
In this lesson
Roy Osherove writes the add always calls logger test first, as if everything already existed, with a fake (external link, opens in new tab) logger that saves the text in a public written field and is passed to the string calculator (external link, opens in new tab)'s constructor. Because the other tests create the calculator through the make calc helper, the new constructor argument changes one place, where a fake logger is passed in as a stub (external link, opens in new tab). He sees only the new test fail, makes add call the logger, and has to add throws Throwable to the tests, which he dislikes.
Transcript
[00:02] (external link, opens in new tab) So what I'm going to do is I'm going to create a new test. I'm going to start with scr from scratch. I'm going to write a test for string calculator (external link, opens in new tab) that adds this dependency. And because this is test driven, I'm going to write my test before I do anything else. So I'm going to say test public void. I need to give it a good name. The name would be well add is the entry point (external link, opens in new tab). I might say always
[00:32] (external link, opens in new tab) Because it always does something. It's not only when a specific something behavior happens. I want add to always call a logger. Then I can say calls logger. Okay. So now what we're going to do is we're going to pretend that the world that I would like to have in my code already exists.
[01:07] (external link, opens in new tab) I'm going to write my test as if everything I need already exists. For example, I might say I would need a fake (external link, opens in new tab) logger. I might say my fake logger. Now I I already wrote this for you guys in the demo. So you're going to see that this class is already written but I might say the fake logger just so that you don't see it in the IntelliSense fake logger mock (external link, opens in new tab) log equals new the fake logger.
[01:40] (external link, opens in new tab) What else? We want to send the fake logger into the string calculator. I want to say string calculator equals I'm not going to use make calc anymore for this because when I use mock objects I want to have an explicit dependency. I want to be able to control the things that I send into the constructor. So I a don't want to use the helper method (external link, opens in new tab) and b if I did choose to use the helper method it would become very very messy very fast. somehow it would have to save the mock object in a in a variable in the class and then I would have to figure out which it would become very unreadable.
[02:16] (external link, opens in new tab) So I'm just going to make it right now direct. So I'm going to say new string calculator and I'm going to send in mock log. Of course this won't compile yet. But remember the point is to finish the test and say I think this is good enough. I understand the problem. this is how I want to test it and then I make everything work according to that. I might say scadd well this is the simplest I can think of and assert equals0
[02:55] (external link, opens in new tab) Into the logger mock log dot written or written and if I'm happy with that I can go ahead and create a class right above here because no one else is currently using this. If I see that I have multiple tests in multiple classes using those fake objects, I can then refactor and move it into a separate file. But right now, I want everything as close as possible to the test so that the serial killer doesn't need to go to my house and ask me what the hell that is.
[03:29] (external link, opens in new tab) Class the fake logger implements logger. Now, logger doesn't exist yet, obviously, but I'm going to pretend it exists for a second. And here I'm just going to say public void write string text. I'm just going to say written is
[03:59] (external link, opens in new tab) Equal to whatever the text is. So, this should be enough. Now I've actually gone ahead and created the slow logger and I do have an interface for logger which I wrote before. So what I'm going to do is I'm just going to connect to that. I'm just going to say logger import the class.
[04:29] (external link, opens in new tab) But I do need a field here. Put it indirectly in this class and make it public. I don't need a getter and a setter because I want this code to be simple. There's no reason to hide and protect anything because this class is only for tests. Now I still have to go and add the parameter to the constructor. So I'm going to go ahead and say that the parameter type is logger.
[05:02] (external link, opens in new tab) I'm going to call it log because the production code doesn't need to know that there is such a thing as mock objects at all. So it has to be purely abstract So that's the difference. Now remember this is TDD (external link, opens in new tab). So I want my test after it compiles. I want to see it fail. So I'm not adding the functionality of calling the logger just yet. If I run this test
[05:39] (external link, opens in new tab) I oh now there's a different compilation error and you're going to find this as well. And remember we talked about maintainability (external link, opens in new tab) of the test. Now I have 10 tests for string calculator but now I've added a constructor argument. Now I would have to change all my tests if I if all of them did new string calculator. Luckily I do have the make calc method right here which is which means I only have to change one place. Now the question is what am I going to send as an argument? The answer is I'm just going to send in a fake logger.
[06:10] (external link, opens in new tab) But this time not no one is going to assert against it. Right? That means that for all the other tests they're just going to have a stub (external link, opens in new tab) logger. Just makes happy noises. doesn't do anything. So now very easily I can just say just send it a new the fake logger. That's it. If I run the test now they should all be passing except the one that I wrote. Where are you? Okay.
[06:45] (external link, opens in new tab) And I can see now expected got zero but actual was nothing. So only my test the last test is failing which is what I wanted. So it took me only a few seconds to make all the other tests pass by sending in a stub and only in a single test am I using a mock object. Okay. And this is where the maintainability aspect comes in. Otherwise this would be hell now to go and send a new one in each one in each other test. So now I can go in and say okay let's go to the string calculator
[07:22] (external link, opens in new tab) And when I call add I can say blah blah blah where is it well I just have to find somehow the default value right so I might say put it right there and say log write dot plus default value and of course this is bad because I'm repeating myself but I can refactor it later after the test passes
[08:03] (external link, opens in new tab) Value to string let's see oh I need to handle the exception Okay. And that means probably that now I have to go and handle the exceptions in all the other cases. Now I have to go here. And this is the part that I hate coming from many different languages. Adding that throwable stuff is just horrible to me. I just don't like it.
[08:34] (external link, opens in new tab) Now I have to go to all the tests and add the throw throws throwable whatever that means. Ah so you see you do it once and then you're done with it but it's still pretty horrible in terms of terms of usability and maintainability. Okay. And let's see if that would work. Start adding.
[09:14] (external link, opens in new tab) Are we done yet? Are we there yet? Thank you.