A new 'Technically Speaking" episode is live! In this session, our teammate Matt explores what it means to mock responsibly. Using a simple Python application, he walks through different approaches to mocking, the trade-offs involved, and how mocks can unintentionally create false confidence in your tests. Want to know more? Watch the full video here: https://lnkd.in/gWnVwag5 #Testing #UnitTesting #Python
Transcript
Hi, this is Matt Morrison with Technically Speaking at Source Allies. Today we are going to be talking about mocking responsibly. In this series we'll be walking through building a simple application and showing different methods of mocking and some of the trade-offs involved with those different methods. When it comes to mocking, there are many different types of names for things. There are mocks, patches, fakes, double s, et cetera. I'm not really going to worry about any of that. I'm just going to call everything a mock and use mocking as the term for when you're tests change the behavior of your application at runtime in order to have better, maybe faster, maybe more stable tests. The first approach to mocking that I'm going to talk about I'm going to call general purpose mocking. General purpose marking is when. You're using. A mocking tool to swap out some code with some other code. Let's look at an example. So here I have. A blank test file. And I have a blank application file and I have my terminal. And I'm going to be using Python. In this example. So first we will start with a test. Fairly straightforward. I'm importing our application, I'm calling our git description function, and I'm asserting that powered by people and slippers exists somewhere in the description that's returned. If I run this. Clearly it is going to fail. OK, so let's implement our application. So in this example I'm using URL Lib. To visit the source allies.com website and read the read the contents of the page and return that. So let's run our tests. And it passes. So in this example we're not using any mocking. This would be a real end to end test that's testing the real functionality of our test. In this simple example, that's probably fine. In a real example, this may be an API that is slow or expensive in some way and we may not want to. Continually recall that over and over. In lots of different tests. So we are going to create another test that mocks. The actual API call. So in my second test. I am going to. Leverage the. Unit test mock library. And I'm going to use the patch dot object decorator. To swap out the URL open function in our application. And then I'm going to explicitly override what that function returns and replace it with this byte string that contains mock text. Then I'm going to call our get description function and assert that the description that's returned contains the mock text that we set up. So let's run our test. OK. So we have one failed and one passed. The one that failed is our new mock test and it's failing because bytes object which is our byte string here. Has no attribute Reed. So in our implementation. The response that we need to be returning. The response that is returned from URL open and the response that our mock also needs to be returning has to have a read function and our byte string does not. So our mock is improperly set up. So this is 1. Downside to using mocks is that you have to. Exactly mimic the. The code that you're mocking. So in this case I've modified our test so that we're using a bytes IO which is a file like object. And we are instead of returning just a bite string, we are returning a bytes IO object. And so let's run our tests with that modification. And it passes good. So this is relatively straightforward, not a whole lot of overhead involved. So let's take a look at our implementation and see if we can break our mock test. So let's say. I inadvertently modify this. So I've inadvertently removed the eye from the URL. And if we run our tests? We see that we have a failing test. Which is our real test because it's unable to actually hit that URL, but we have a pass test which is our mock test which. Is not ideal. Preferably we would want our mock test to be. More reliable so that if the the real test is not going to work, if the real code is not going to work, we want our mock to give us that quick feedback that it's not going to work. So let's modify our mock test so that it does fail and it does alert us of this. Um, again, if we assume that our end to end test is maybe a test that we're only going to run in our CI CD pipeline, we're not going to run it all the time as we're developing. This could be something that. May have gotten missed. So if we modify this test, I'm going to add this additional assertion to our test. That asserts that our URL open function that we're mocking. Is called with sourceallies.com. And so if we make that modification. Run the tests. Now we see that we have two failures. We have the end to end test is failing as we would expect and our mock test is also failing. And it's actually giving us some pretty decent feedback. It's telling us exactly that we expected this URL to be used, and we used a different URL so we can easily see what's going wrong and we can correct our error. And run our tests and see that now we have 2 passing tests. OK. Next up, let's say in our hypothetical application here that the library that we're using to do whatever it is our application does is being deprecated and we need to replace it with a different library that does a similar thing. So in this case, it's just a hypothetical. In this case, we're using URL Lib and we are going to need to switch to using the requests. Library to do our web requests. So let's update our test to. Instead of mocking URL open, we're going to mock requests. OK, so I have. Our updated test here. So not much has changed. Instead of mocking URL open, we're now mocking requests. Um, and we're setting our exact same return value on requests dot git. And we're making our same assertion about our URL on requests dot git. And everything else is the same, so let's see what that looks like. OK, so obviously it's failing because we don't have, we're not using requests yet. So let's go ahead and update our application. To use requests. OK. Right away our linter is giving us some feedback, which we're going to ignore for now so that I can make a point later. And we're just going to run our tests. OK. So we've got a failing test and a passing test. So right now the real test is failing, but our mock test is passing. Again, this is not a good scenario because our mock test is giving us a false positive, telling us everything is OK when it is not. But. The advantage here is that we have guide rails in place. We have our end to end test which is going to provide us feedback, but potentially slower feedback, but it is going to. Keep our mocks honest and make sure that they are. Accurately. Reflecting how the application really works. And we also have our linter which is also providing some feedback to let us know that we're not actually using the response from the requests dot get function properly. So. With this type of error, I will go and look at the requests libraries documentation and figure out how to properly use. The response object. And after reviewing that I found that. I actually need to be setting. The content not using a bytes IO object as before with URL Lib. And I just need to set a byte string here instead of the the the bytes IO. So I'm setting the return value of the git function call. To unlock that has content that is my bite string here. OK, let's save that and run our test again and see where we're at. OK. So. They're both failing now, which is good. That's that's what we want. We want our mock test to be consistent with how the application really works. So they're both failing, which is good. The assertion here is a little ugly because we have this weird magic mock request dot git dot read dot decode, which isn't as nice as the other area that we had when the URL is incorrect, but at least it's letting us know that something is wrong. So let's go ahead and update this to have content. And so now everything's passing, everything's happy. Our linter is happy, our end to end tests are happy, our mock tests are happy. Everything is good. OK, Next up, let's take a look at. Refactoring. So as I was going through the requests library documentation to figure out why the request dot get dot read wasn't working, I also discovered that there's another handy method that allows you to just say response dot text and not have to do response dot content dot decode, and so I'd like to use that. So that should be a relatively simple refactor. I can just swap this out and say text. And run that. And our mock test is failing, but our real test is passing. So this is kind of the opposite scenario we had earlier where the mock test was passing and the real test was failing. Now this is the opposite where our mock test is failing, but it works like it really works. But our mock is set up incorrectly. So the unfortunate thing. About the way that this mock. Is working. Is it has a lot of intimate knowledge about exactly how we're accessing these objects. And. If I want to make that type of refactoring, I'm going to have to change every test and if I have. Multiple of these tests. I'm going to have to update all of them. So I can make this pass, I can say. Text. And this actually is not quite, oops, not quite going to get us there. It's like object is required, not a string. So I don't need to use a byte string anymore, I can just use a string. Up there now we're passing so unfortunately. This is one of the downsides of using this type of mocking, this general purpose mocking. At least in Python, your mock has to have intimate knowledge about the objects that it's replacing, and it has to behave exactly like them. Otherwise you may get false positives or false negatives, both of which you want to avoid. So this has been an example of general purpose mocking and some of the trade-offs and some of the guide rails that you can use to keep your mocks in check. This has been Matt Morrison with Technically Speaking at Source Allies. Do you have a topic you want to see covered? Comment down below. Check out Source allies.com to learn more about us. Thanks for watching and we'll see you next time.To view or add a comment, sign in