BrandGhost

How You Can Make ANYTHING More Testable

"I can't add tests for it, that code is untestable" or "that's in the legacy code, I can't test it" - You've no doubt heard (or said) these things before. But what if I told you that we can, with a tiny bit of effort, make ANYTHING more testable than when we arrived? Let's walk through this dotnet example together to see how! For more videos on programming with detailed examples, check this out: https://www.youtube.com/playlist?list=PLzATctVhnsghN6XlmOvRzwh4JSpkRkj2T Have you subscribed to my...
View Transcript
what if I told you that I had a pattern you could use to make any code that you're touching more testable than when you got there well in this video I'll show you how so that you never have to go check in code without tests hestable code is a big theme on my channel and also in my career so I want to start with a story here I've worked at both startups and in big Tech and this is a common thing that I see across the board so it's not just restricted to new smaller companies working in brand new code bases and it's not restricted to companies with just Legacy code this comes up all of the time when people are making changes and they happen to be inside of an area of code and they say well I couldn't add the test because the code just isn't testable and an even more dangerous thing is that as this pattern propagates you have people continuing to commit code to an area and saying well I can't test it because it's not testable the previous parts weren't tested so on so forth more code added no tests added if you fast forward over time what ends up happening is that this you usually becomes a brittle area of the code no one's excited to go make changes there because things often break and you find out in production from a user and not from your tests so in my travels I got tired of hearing this because I figured there must be a way to at least make what you're changing testable this pattern actually came from working directly with testers more as strategists and less about running test cases manually so by having a lot of trust between our engineers and testers and a high degree of collaboration what we came up with was this scenario where we would say look like I've fixed the bug and it might be something that's really difficult to reproduce and we'd be able to kind of walk through the different things the tester might be looking for and then we could actually go show them that I've created code that will go cover these tests and because in some cases we were working in Legacy areas of the code or we were working with systems that we didn't actually directly control so say that we were fixing a bug for a customer and that's not something we could reproduce locally a tester would have a lot of difficulty going to try and prove that it worked but we actually had some superpowers because we could say look if I can demonstrate to you in the code this is how this exception occurs or whatever the situation is now I can show you with test coverage that it's there and fixed so with this pattern what we're actually able to do is add test coverage over the changed code but what I'm not saying here is that I'm going to fix all of your legacy code base and it will allow you to test all of it at once what we're going to be doing is isolating the parts that we're changing so that we can test those and that way you're leaving the code in a better spot than when you got there and the changes that you're creating actually can have tests and there's no longer an excuse to say it can't be tested because it can the rest of the code at that point in time maybe cannot so if that sounds interesting let's jump into Visual Studio I'll walk through an example and we can see how this works all right I'm here in visual studio and what I've done is created something like a legacy system I've also left this big comment at the top that I want to pause and make sure we take a moment to read Because when we're creating content like this and putting it out on the internet I don't want people that are maybe skimming through stuff to see some of this code and go oh that's how that works that's how I should do it in fact this is an example where it's the exact opposite so my disclaimer here is that this code that we're about to look at is absolutely something I would not recommend you follow and that's because I'm just trying to demonstrate with this example that you have come across a change you need to make in a legacy system its Legacy hasn't been touched in forever so people have learned about how to do things better so there's going to be some things that uh you know you would probably not do if you knew better and that's okay and the point here is that we don't have to go touch all of the code to go make a fix and we don't have to go fix up in boy scout or Girl Scout all of the code that we're looking at just to be able to make our change and add some tests over time as this becomes more and more testable certainly you could go take that approach but my point here is that I just wanted a disclaimer because some of the stuff we're going to look at like you know some of this stuff even right here like I would not recommend you go write this in your code to accomplish what these things are supposed to do so with that out of the way let's look at this Legacy system it's definitely just a you know a super contrived example because like what we're going to look at here is not really a real thing but I wanted to kind of use some things that generally make it harder to write unit tests so the reason that I want to look at making things unit testable and not necessarily like how do we make it more you know functionally testable or you know able to support by integration tests is because when we have Legacy systems sometimes depending on what's making it Legacy or what some of the code there looks like actually writing integration tests over it might be very challenging because you don't have control over a lot of the pieces so for example in this case I'm not connecting out to a database or anything but if you had a legacy Code system and you did not have control over how that database connection was set up you're going to have to do refactoring to be able to have some control over it because maybe you don't want it to code directly to production that kind of thing so if we can focus on making things unit testable this way when we make our changes to fix bugs or add features whatever it happens to be whatever code we're directly touching we should be able to go ensure that we have unit tests over that directly again over time as you start to follow this pattern and this practice the more and more that your legacy system ends up becoming broken apart into smaller pieces this might allow you to functionally or you know write integration tests over these pieces more easily and that's because in isolation they're now testable with and that's because in isolation they now have unit tests over them so you can ensure their functionality within their methods and classes but beyond that now you should be able to kind of write the integration between them when it's all set up in some type of Legacy system that was never really thought through to be testable that can be really challenging so in our Legacy system we just have this one public method on here it's just do something async what we're going to be doing is actually working with a blog post but the point of what we're looking at is not actually the work in particular it's just so that we can point out where we find the bug how to fix it and how to write tests for it so when we're thinking about things being unit testable there's a couple of things that kind of stick out to me that I don't I never like saying you know you can never do something you should always do something but I have guidelines so when we have things like static methods to access the file system generally this makes it harder to unit test because in a true unit test you don't want to be working with things outside of the code so going out to the file system generally is kind of going uh you know around the boundaries of a unit test you can still absolutely write coded tests in your perspective that still might be within a unit of work but in my opinion once you start touching the environment like the file system or going to the internet over the network that's kind of going outside the unit that we're testing so static methods for the file system generally not so great make it more difficult using date time now so again static properties to access the current time it's not that this is impossible to test around it's just that it makes it a little bit more difficult because if you can't control the current time it makes it really hard to force the code execution to go into this path and actually have assertions that will check what we're looking at here so again common theme is that static methods static properties it doesn't doesn't mean you can't use them or you never should or anything like that but think about your usage of them when it comes to testability so in this case we're just saying hey is there a no blog file here saved as an HTML file and or I guess in this case or if it happens to be a Saturday we want to go get it anyway so then there's this method to get some blog article now the code after that we'll jump into this in a moment but the code after that starting on line 18 actually tries to read in the text from the blog HTML file it's going to call this process HTML async method with that HTML and get it back out of that method and then it's just going to go right out that HTML again and you'll notice that I have another static method to go right out to the file system so just like above this can make it a little bit trickier to go you know write true unit tests over top of it so the reason that I planted these things into the code was just to kind of show you that these are usually practices again another static one here that make it a little bit more difficult to actually write unit tests over doesn't mean it's impossible doesn't mean you can't write other tests but it generally makes it a little bit more of a hurdle if we go look at the process HTML async method you can see that we're just passing in that string I wrote a comment here saying like I'm not actually going to go write a whole bunch of logic for this example we're just going to to lower it but assume that you know we had some method in some Legacy code base I actually had to go operate on this HTML the body of this code is not super important I just wanted to illustrate that in a real program you might be doing some other work here now the other method we have is get some blog article again I used static here and at this point you might be going wow this guy really hates on the static keyword not necessarily I just like I said I think that when you have stuff like this it can make it a little bit more tricky to test this is a private method so in the first place it's a little bit trickier to actually directly test if our class was small and the method in this class the calling into here was also pretty small then perhaps this isn't so difficult to work with especially if we had dependencies that were passed into this method that wouldn't be so bad either because we might have control over them so another thing for testing and testability is that when you start newing up instances of objects that you don't have control over right so from a unit test you would never be able to mock this out or have direct control over it you kind of just need to let it do what it it's got to do when the code's executing this is kind of like a static method in the sense that we don't have control over it so this is one thing that we have right at the top of this method that's going to make things a little bit trickier for testability you'll notice too this is a static void method this is an async method I'm not awaiting it I'm just saying get the awaiter get result so interesting right why would we do that I don't know this is Legacy code someone wrote it and they didn't know any better and then I planted a bug directly in this code on line 40 and left a comment there and what we're doing is because we're pulling a page for my blog we're actually going to replace Dev leader with some total nerd obviously that would be a bug because that's not cool if someone were to do that and we would want to make sure that we can fix this up at the end of this method we're going to write all text for blog HTML with this value here that is literally what we pulled from the internet replaced with this part here you'll note too another usage of static for write all text so just a common pattern throughout this whole thing static methods and then newing up dependencies that we don't have control over make things a little bit trickier to test so let's talk about this code for a moment if we know that the bug is right here right and right now we don't have any tests over this class because it was really hard to test when it was originally written no one wrote tests because they were like hey I don't know how to mock out this HTTP client because it was nude up I don't know how I'm going to mock out all this file system interaction because it's actually touching the file system G I had to use date time now I'm not really sure how I can control the code execution path coming into here and then they said they couldn't write functional or integration tests over this class because maybe they're continuous integration system doesn't have internet so it was just left never touched and never tested but if we look at where the bug is if we said well to fix this bug all that we have to do is go remove this code now the question you might be asking yourself or if you're working with other people on your team and talking about test coverage test strategy that kind of thing is like well you've fixed the bug right you might be able to go run this locally and say cool it all does what I expect it no longer says this guy is a total nerd so how do you prove that that's actually fixed how do you prove that you're going to have regression coverage over that type of thing right so the regression coverage doesn't necessarily have to be like hey look it it will never say this guy's a total nerd again but you might be able to say like we would have caught this bug if we could have actually tested some things about this method in the first place and that way we wouldn't have that bug slip in well if the only part of the system we actually touched was right in here in theory to make this just a little bit better all that we would really need to do is write some test coverage over this method now you might be sitting there going well Nick that's not going to prove that the other parts of the code didn't break and this is still all going to be untested and you're absolutely right like I said at the beginning of this video the point of this strategy in this process is not to actually make everything more testable all at once and it's not to make a claim that by writing these tests all of a sudden you're going to have a hundred percent confidence over all of your legacy code what the point of this is is that you can make changes inside of any Legacy code or other hard to test areas and make your changes specifically more testable so the pattern that I like to use to get started has actually already been performed but I want to talk about it because it's usually a good first step because our code that we wanted to fix up is inside of this small method already it's kind of already done for us but if it were say something like maybe this line here line 18 if for some reason this was the the line that I needed to change or make adjustments around that might be inside of a bigger method that I'm not going to be able to go test all at once and I might want to isolate just the part that I'm touching so what I might do is go pull that code out you know I'm not going to necessarily pull out a single line but just as an example maybe it was this much code I might go pull that out into a method so in our case we actually already have a nice little method now this one is pretty purpose built it's just going to go fetch a Blog article from the internet and write it to disk like technically you could argue that's doing two jobs but you could maybe make the argument that hey the single responsibility here is translate internet article to file on disk so cool that's pretty close to single responsibility that feels pretty good and maybe we could go look at testing just this now because now we know that its responsibility is to go pull an article from the internet to save it to disk and that way we can go make sure that it works as expected and if I pause there for a second if you think about what I'm doing is I'm just kind of decomposing all of the responsibilities in this class and looking at the methods and saying can I now have it at a point where I can prove that this smaller piece right we're just making it into smaller pieces can this smaller piece be tested and this process almost becomes recursive in nature because now we go look at just the code that's highlighted and we go talk about some of the things that I said earlier that make it hard to test or easy to test so in this case we're newing up an HTTP client so again that's going to mean that I don't have control over that from a unit test perspective it means it's truly going out to the internet if we were thinking about you know cleaning up the code as we're going so like that Boy Scout Girl Scout kind of approach well this is not an async method it's a static void method and we're kind of just cheating and not using the async await part we're just getting the awaiter and getting the result so that's not super great and then the other thing is like we said earlier this static method so if I were having a conversation with someone on my team and saying look I want to change as little as possible just to prove that I fixed this and write tests for me this actually might be okay to totally leave in there and say look I can actually just go make an assertion in the test that will say hey look this blog article has been written out to the file system this part up here I might not be so comfortable with and this might depend a lot on how your test infrastructure works right if your team or you are totally cool with having tests that go fetch data from the internet you're willing to assume that this resource is always going to be available that kind of thing like I don't think that's necessarily a great idea because well unfortunately my blog has gone down and you don't want your you know integration suite for your test to just start breaking builds because of something like that if you're comfortable with it I'm like I'm not going to tell you no but in in my code base I probably would not be trying to do that kind of stuff that would you know gate builds especially so I might try to leave this in place I might look at how we can mock something like this out and then one of the little things I might clean up is if I can make this async a weight instead of void and if we check out the only spot that this is called well it's already a link so that might be okay to clean up so at this point what I'm going to do is I'm just going to copy this method I'm going to comment it out I'm going to say old and I just want to leave that here so if we need to refer back to what we can I've now pasted the method again and I'm just going to delete the code where the bug was so this is what we were just looking at and I'm going to leave some comments in here that say what we're going to try to clean up okay so I've just left a comment here talking about the HTTP client so something we can do to not go to the Internet talking about making it async away because that seems like it should be a little easy win for us and then the other thing was if we can not actually going out to the file system so what I'm going to do now is I'm going to go add a test project make a little bit of a setup so that we can call into this and then from there we're going to start refactoring the code so I've created a test project and we're going to be using X unit here so if you're not familiar with X unit we can Mark our test methods with this fact attribute and I've made other videos about this and I'll just show you briefly because I can link above but if we check out the project we can see that I have these package references here this is what I use for setting up my tests I know that some people are going to see this line right here and especially all of the information from the recent weeks people are probably going to be upset about seeing that I'm still sticking with mock I mean do what you want um I'm just comfortable with this and I'm not really trying to jump into all of the drama so you don't have to use mock you can use whatever you want but X unit is the preferred package that I'm using for my test framework and let's jump back over to the tests and think about how we want to go test our class now given that this is a legacy class I did call out earlier that we're not going to be able to go tackle the entire thing right so the goal here is not to go say how do we run tests across the entire Legacy class because I actually tried to design it such that it would be difficult to do so but we did make changes to one method and we should be able to try and at least write coverage for that so that we can talk with people on our team to say hey look I fixed the bug or if you're delivering a feature or something else you can at least say based on the code changes I've made we now have test coverage that we didn't have before now if you recall the Legacy system did have a method called do something async and we can await this but that means that we have to change this to be an async task as well and you can take note here that I didn't actually name the test yet but we'll get into that as soon as we get a little bit further now the challenge that we have is that this is the only method that's publicly available on Legacy system and that means that if we want to go testing just the method that we were working with we actually don't have access to that and this is where some slight refactoring is going to come in and like I mentioned a couple of times earlier in this discussion we can take this approach and start reapplying it to different scenarios and kind of this recursive pattern so if we jump into this method right this is what we were looking at before this is the method that we ended up fixing up to remove that line that was the bug and this is actually what we want to try testing so my recommendation and this is where you can't apply this necessarily as a blanket rule all of the time without thinking through it but what we want to do is actually pull this out as a new dependency the reason this takes some thought process if you kind of extrapolate what I'm talking about here if you were to try removing just one or two lines all of the time from your legacy code and try and write some tests on it you're probably going to run into this situation where instead of one big Legacy class that you can't run tests on you might have a hundred 200 classes that are tiny that all have test coverage on them but they're totally incomprehensible with how they all fit together because they're just not thought through so that's one of the drawbacks of this if you don't put some thought into it but like we were saying at the beginning of this because we took this method and we were saying actually this does actually have a single responsibility which is to go fetch a Blog article and have it on disk we can say maybe we want to go have a blog fetcher class they can fetch a Blog so what we're going to do is pull this method right out of this class I'm going to introduce a new class right so now that we have this new class we can drop our method in and now that we have our method on here what we can do is go public Boyd get some blog article now all of a sudden we're in a situation where this code is isolated and we can start to talk about how we test this directly instead of all of the Legacy code so this is an important checkpoint because this is the concept that I want us to be able to apply over and over as needed and again we want to think through whether or not we have small classes that actually make sense because you might want to group some things together as you're moving along one other thing to note is because we pulled this out the other code is actually broken but that's okay because we can fix it up just by passing in that dependency so let's go ahead and do that so to pass in the dependency we have a field and then we're passing in through the Constructor a Blog fetcher instance and then all we want to do is use this blog Venture at the point where we're calling the method and now all of a sudden we've isolated this bit of code the blog fetcher that has our method that we touched from the rest of this Legacy system and if you think about it we're not actually going to be writing tests over this part of the code so that's still going to remain untested it's still going to be something you're going to want to have a conversation with the team members about to make sure that you all have confidence in this change because yes this is a contrived example yes in your situation it might be a little bit more complicated you might still have to touch a little bit around here but at least you're adding net new test coverage especially over the code that you're touching let's jump back over to our tests and actually change the fact that we're not going to be testing our Legacy system we're actually going to be testing a Blog fetcher alright so blog fetcher gets nude up here on line 10 and then on line 11 we actually call get some blog article but recall one of the things that we were thinking about tweaking as doing some boy scouting or girl scouting was that this is not an async method well let's go ahead and adjust that right now I add async task out the front here instead of void and just because I like to stick to to this naming convention I am going to stick async on the end of this so it's now get some blog article async and and we can actually look at changing this to write all text async and awaiting that as well the other part that we called out from before is of course that we don't want to have this getaway to get result so let's go ahead and backspace that and we can actually await this part here and just to fit a little bit more text on screen I'm just going to split this string across a couple of lines okay so what we've done so far is we've now made this part async await so let's cross that off our list now we still have an HTTP client used here and we still have this fact that we're touching the file system but we did say that we might come back to this one and based on our types of tests we might leave this one still because realistically just writing the file to disk we might be okay with that now a quick note before I forget is because we did make this async we are going to want to go look at where it's used again and actually change that to be awaited if you recall from earlier I did say that this might be an except change because this method was already an async task and if we're already right here we might be able to just await that and get some goodness out of doing that if we go look at our test and we await this line we'll want to do the same thing on our test and if we think about what we have written here now we're actually in a test method that can go call this method and that's great but now we want to think about how we're going to assert what's actually happening so two things going on are one that when we call this it's still going to go out to the internet that's one of our line items to address and number two is that we're going to want to see if we can validate the file that's on disk after this or look at mocking that as well first step is going to be addressing the HTTP client generally the strategy that I like to use when I see this type of situation here is I like seeing if there's an interface already implemented on the object that I'm dealing with and that way I don't require that I'm dealing with a whole new instance and that my method is responsible for creating that instance so if we go press F12 in Visual Studio video to go look at the definition of this if we look at the class up top here HTTP client and then message invoker up top here as well well what I'm actually interested in seeing is if there are methods that allow us to get strings or if we have to go dig a little bit further to see if there's an interface that implements these and currently what I can see is that the methods are actually implemented directly on this class and not part of an interface if I do look at the class that it inherits from there's a little bit of proof here that we only go up to eye disposable and there's no interface in the class hierarchy so that's going to mean that we can't actually just go pass in an interface for ihttp client because there isn't one and that's okay in this particular case just to demonstrate an example all that I'm going to do is show us repeating this exact same process but for an HTTP client and again I just want to call out that I'm not necessarily recommending you do this specifically for an HTTP client in my opinion if you want to look at mocking out your web requests and things like that there there are other Solutions you can do instead of rolling your own wrapper every single time you want to deal with an HTTP client but let's have a little look at just a simple solution to walk us through this all right what I've done so far is that I've made a new class called an HTTP client wrapper that takes in an HTTP client and calls a method gets string async right on the HTTP client and that's because the only thing that I'm using in my existing code is get string async from here because I'm using Mock and we're not using some of the newer features of.net that are coming which allow interception the way that we look at mocking currently has to deal with interfaces and lots of people have different opinions about overusing interfaces personally I do like using them for testability and mocking it's one of the strategies I find works really well for me this is something I talk to other people about so that they have that tool in their tool belt to be able to go use but of course you should find what fits for you in this particular case we're we're going to look at pulling out an interface from this client wrapper and just by adding that interface now we can see that we have I HTTP client wrapper I'm going to rename that just so that it says ihttp client and we have this single method get string async so now you might be saying well Nick this part is still not testable if we were to call anything on here we're still going to go out to the internet and you're absolutely right and what we're going to be looking at is the same concept that we talked about in the beginning we're not trying to test every single piece of this we're just trying to isolate our code so that we can write code over top of it to execute our changes specifically so the end result of all of this is that we're not going to be testing any web request stuff not with our unit tests at least what we are going to be able to test is that we have some flow over this method that does what we expect so now in this case our blog fetcher will take in a new dependency so we'll pass in the ihttp client and then we're going to replace the HTTP client usage with our new interface okay awesome so now we don't have a concrete HTTP client in here that means that we can technically go write a mock for this and not go out to the internet if we don't want to for this case so what we're left with is something that's going to call a mocked interface for an HTTP client and we still have this write all text to disk we are going to leave this writing to disk and the reason that I want to leave this at least in place is that I think some people watching this might say by the end of it well there's actually nothing left in here what are you actually mocking what are you actually testing and I think that's a valid point generally I would probably try to clean this up as well just to prove that I don't actually have to go manage files on disk between my test runs and things like that but if we go do that we're just going to be left with two dependencies we're mocking out and it might feel a little bit silly to some people so let's go ahead and jump back to the tests and see what we can do our blog fetcher now takes in a dependency that we're not passing in here so I'm going to go ahead and create a new HTTP client but we're not going to pass in the HTTP client wrapper like this and that's because if we go to create it exactly this way what we're left with is still going out to the internet instead we're going to use Mock and like I said if you're not a fan of mock anymore you can go ahead and use some other Library that's totally fine the concepts will still apply so I start by creating a mock repository and I use strict on here so that I can ensure everything I've set up is actually correct line 13 now actually creates the HTTP client Mock and that means that on line 15 we have to make a slight adjustment which is actually passing in the mocked object property at this point in time we could technically go run this test and what's going to happen if we were to go run it is if we check out what this method actually does now by the time we hit this part of the code right here we're actually going to get an exception and that's because we didn't actually set up our mocked http client we just created an instance of the mock so for us we're going to want to actually ensure that we have this setup configured properly in our test we can talk more about that in a moment and then the other part is that it's going to write out a file to the disk once this mock setup is actually working if we jump back to our test we can think about how we want to actually validate the output so two things I'm thinking about doing are number one checking to make sure that the file exists and then the other part is we can talk about actually reading in some of that text and doing something with it now again this isn't necessarily something I would be trying to do in these tests I might not try to be interacting with the file system like this if it were something like a true unit test in your situation if you're finding different ways that you can at least get test coverage on this code then I think that that's way better than nothing again for this type of thing you'll want to consider how your tests are structured and whether or not this is applicable for the area that you're working in all right so what's left in this test is that we probably want to consider what it means to be valid when we go to to read in this result and the other part here is that we have a mocked HTTP client this wrapper that we made and we're going to have to set up that mock such that when we exercise the code inside of this method it behaves how we would expect now just a quick note on mocks and how you can use them for your test setup generally one of the things that I really like about mocks is that you can control the behavior of your dependencies and that might sound really obvious and it's not going to be demonstrated very well in this particular example but if you needed to be able to control your logical flow to go across if statements so for example so if some HTML came back as null and you wanted to throw an exception or do some other type of processing right I don't know maybe you want to set it to some default value whether it's empty or you want to put whatever in here when you're able to write unit tests with mocks and you can control the different results that come back based on conditions you're actually able to force your code into certain paths and in our case it's not really going to be applicable but that's generally the idea why I do like being able to write unit tests and I say this all of the time but I like writing unit testable code but it does not mean that I go right unit tests for absolutely everything in fact it's pretty rare now that I am writing unit tests but if I find that I need to go back and actually Force some code execution paths I really like the ability to be able to do that without having to refactor all of my code at that point so just a heads up that's why I like writing unit tests with mocks but we're not going to have to do that in this particular case to control the flow the mock we have to set up is going to be this HTTP client and specifically this get string async method now because this is a pretty contrived example I did hard code this string in here but that means that in our setup this is the only string that we need to look for in fact any other string that was passed into here we might consider that a bug actually so when you're thinking about setting up your test us you can consider whether or not you want it to be valid for other strings to come in here or if you do actually want to guard against that the other part is that our mock is going to have to return some HTML so let's go back to the test and set that up this setup rate here is by default when I used copilot would it just spat out for me and I just want to talk about it quickly because I think it's close to what we want and in fact it might be okay for this example but I just want to speak to this it dot is any part really quickly if you recall I said that we can only expect one URL to be passed in at this point in time now if you consider that behavior expected then it must always be that exact URL then I would highly recommend that you do not use it.is any but instead you actually put in the string that you expect so in this case instead of putting it dot as any we would actually go put that URL in and that way if anyone came through that code and they changed the URL in any way our unit test would actually break and that's to guard against accidental changes because we're saying we actually expect this exact URL but in this case just to make things really simple I'm just going to leave it as it dot is any but I just wanted to call out that that's why I do it that way the other thing we'll note here is that it returns async and the string that comes back is some HTML and what's interesting here is that given that we know the actual structure of our code that's being executed we don't actually need it to be real HTML we can kind of do whatever we want here because the reality is whatever is here on line 17 that's what we should be expecting red back from line 23. so in this particular case this is what I would expect because the only thing that we end up doing is taking this result directly from the HTTP client writing that out to a file and that should be exactly what's inside of the file when we read it back and just to wrap up this part for the test one thing that I always like to do with my monks is actually call this verify all and the reason that I call verify all is that if over time you're refactoring code and things are changing if you had extra mock setups say something like this and we had another setup here that was some other URL and at some point in time if it was valid to have both of these setups like this if you didn't have verify all what could happen is that over time you have all of these extra setups that might exist and if you're not cleaning them up they never really go away but with verify all if this one's actually not getting called verify all will tell us and actually fail the test and it will say that this mock with some other URL was actually never called so it's not that the logic under test has an error it's actually telling us that our test has extra stuff set up so at this point in time I think that this is actually a test that would cover our behavior that we have inside of this method that we pulled out into a new class this is a little bit of a silly example because the bug that I was trying to prove is fixed was actually just some Silly String replacement but in your particular case you might be able to reflect on the code that was being changed and actually do some other types of assertions here that prove that things are working as you expect when it comes to naming this I might go change the title to get some blog article async because that's the method that's going to be executed the next part is generally where I would put some type of conditions about the setup that we're looking at and then the last part is just some string that's going to tell us a little bit about our expectations for the result this might not be the best naming for this particular example but I just wanted to demonstrate that instead of some placeholder text if I go run the test we can actually see that this test now passes and that's awesome and it's not really that complicated but like I said this is just to show you the pattern for pulling the code out to be able to test it before summarizing this video let's have a quick look at what our Legacy system is left looking like and this is going to be something that you can think about for the conversations that you either want to have with testers on your team or if you don't have dedicated testing roles or anything like that you can talk with other software developers to make sure that you feel good about the coverage you have now the bug that we identified had nothing to do with this particular method right here and like we talked about in the beginning of this video this method we were saying Hey look it's a lot of Legacy code it's going to be hard to test it because some of these static things we have some things that are specific to the time it's just going to be a bit of extra work to try and clean all of this up and we'd have to refactor and rewrite basically the whole method that might actually be okay in this particular example because it was pretty small and pretty crappy code to begin with but if you had a bigger Legacy system the point here is that we're trying to leave this part mostly untouched and unfortunately in our particular case this code didn't get any easier to test so we didn't end up adding any extra tests specifically over this method so this is a point you're going to want to talk about and say hey look like I made changes and this code path is going to call them now I wasn't able to test this code path still all up however the part that I changed in particular I was able to write tests on and what we did was because that code was actually already pulled out into its own method if we scroll down a little bit this is the old one that existed right we actually pulled that out into an entirely new class and it's sort of a purpose-built class and that was so that I could demonstrate some boy scouting and girl scouting kind of cleanup that we could do very minimal for the async task part of the API there that way we can async await it because the original code was a little bit gross then we actually looked at changing up the HTTP client so that we could inject that into here as a mocked dependency for our tests and again people have different opinions about this newer.net versions will actually allow you to use interceptors there and not require interfaces so lots of different options on the horizon that's totally cool but in this particular case we now don't have to go out to the internet when we're running our tests but one thing we left in place here and this is where I was trying to say hey look this might not be something that I always do but we didn't actually have to go refactor and pull this out and sort of do the same thing where we mocked out that behavior I kind of Drew the line at saying I'm okay to interact with the file system sure it's kind of breaking some of my air quotes here unit testing principles that I like to follow but it's still allowing us to get some test coverage on this method so if we're talking within our team about if this method is working better or not well sure we're not actually proving again with the unit test that it's going out to the internet quite literally we made a choice to mock it but you might be able to explain to your colleagues and even for yourself to have confidence that the rapper class we created is literally just a direct call into the HTTP client so no custom logic there and I mean looking at the URL if we thought this was working before and that wasn't the problem it should still work after this refactor if people weren't happy with that you might be able to say cool maybe we do want to put an integration test on this maybe you do have a test Suite that actually allows you to go connect out to the internet and then you don't need a mock you can actually just call this thing and have it connect out to the internet and everyone's happy with the test coverage that's for you to decide within your team and then because we pulled out the line of code that was doing the Silly String replacement that was demonstrating the bug we actually don't have an issue anymore and our test actually validates that whatever comes out of this HTTP client regardless of whatever it is that's going to be exactly what's written out to the file so if we jump back to the test super quick that's why this part here that actually reads in the text from the file equates it to exactly what's returned here so to summarize in this video we looked at a pattern and practice that we could follow that would allow us to go refactor code to pull it out of what would be considered Legacy code I came up with a really silly example to demonstrate this but I'm sure you've come across some Legacy systems in your their programming experience already where you were like holy crap I don't think I can test this the idea was really just about extracting code so it could be isolated and then there we could go run tests over top of that extracted code I demonstrated this with something close to a unit test showed some code improvements we could kind of make along the way but I just want to call out you're not forced to write unit tests to do this please use the tools that make sense if that's unit tests and that's what you follow great if you want to write integration or some other type of functional test over top of that because that gives you the confidence you want by all means do that the entire Point here is that we could actually isolate that because that's where our changes were and then run tests across it what we're not doing here is proving that we have the rest of the test coverage across the Legacy part of the code so yes there are still untested Parts here 100 not trying to claim otherwise but I think that's a really good conversation opportunity that you can have within your team and try to build confidence along the way so you can demonstrate to your teammates hey look we're cleaning this up a little bit as we go yes it's not fully covered but it should be net better than when you started there so I hope you found this insightful thank you so much for watching and if you have any questions please leave them below and I'll try my best to answer thanks we'll see you next time

Frequently Asked Questions

What is the main goal of the pattern discussed in the video?

The main goal of the pattern I discussed is to make any code that you're touching more testable, especially in legacy systems. By isolating the parts of the code that you're changing, you can add test coverage specifically for those changes, leaving the code in a better state than when you found it.

How can I deal with legacy code that seems untestable?

When dealing with legacy code that seems untestable, I recommend isolating the specific parts of the code that you need to change. You don't have to fix all of the legacy code at once; instead, focus on making the changes you're working on testable. This often involves pulling out methods or creating new classes to encapsulate the functionality you're modifying.

What should I do if my changes don't allow for full test coverage of the legacy code?

If your changes don't allow for full test coverage of the legacy code, that's okay. The goal is to improve the testability of the specific parts you're working on. You can have conversations with your team about the areas that are still untested and build confidence in the changes you've made, showing that the code is in a better state than before.

These FAQs were generated by AI from the video transcript.
An error has occurred. This application may no longer respond until reloaded. Reload