 I'm here today to talk to everyone about your most favourite thing, legacy code. No matter where you are on your software engineering journey, whether you're a newbie or seasoned developer, you are going to encounter legacy code. It's not a case of if it's when. It can be really overwhelming though. Legacy code is horrible. When you see it, it just confuses you. You don't know what's going on. It's just horrible. Today I want to talk to you about a lot of tactics to help you tame the beast that is legacy code. Hopefully you can start viewing it as an asset to your company and not just something horrible. First of all, I want to talk what exactly is legacy code. There are a few definitions for legacy code. Legacy code, in my view, is any code that you didn't write just now, and I mean within the past five minutes. It becomes legacy pretty much immediately. It could also be code that's resulting from incorrect assumptions, code that was built using unsupported technology, for example PHP 5.6 now, and also code that isn't tested. Refactoring is something that we're going to do a bit of. What is refactoring? It is updating an existing code base, but in such a way that it doesn't actually change the application logic. You can use it to improve the code structure, and you usually approach it in very small incremental steps. Why would you refactor? To improve other developers' understanding of the code that improves developer velocity and get more done. You can also improve the application's performance. So, for example, if you're changing an algorithm slightly, you achieve the same result, but in a more performant manner. You can use it to introduce new technology, for example, upgrading your applications to PHP 7, and it also helps you to better accommodate changes in your code when things are just generally a bit more sane. Unit testing is also what I'm going to cover today. Unit testing is a verification that each individual unit of your application works as you intended it to. A unit in a definition of a unit would be a component part of your software application, so, for example, a function or a piece of behaviour. Just a small note here is that I am going to be talking about unit testing during this presentation, but unit testing is not a substitute for all other types of testing. There are lots of different ways to test your code and your application, and this is just one of them, so please bear this in mind. I love unit testing because it helps you to document your system. You can look at the test and see what people intended to be happening in your application. It also helps you to prevent regressions, so if you change something in your code and it breaks something, your unit tests are hopefully going to alert you to that. You can also use unit tests to test integration in new technology, so if you're upgrading your application. It also helps you to validate your product requirements. There's a slight paradox when it comes to unit testing and legacy code, in that you can't write the tests for your code because it's legacy, and it's untested, and it's horrible, and it's really difficult, and you need to refactor it. You can't refactor it either until it's tested because things could go wrong. It would be horrible. I've got a solution for you all. We're going to rewrite the entire code base. Sounds like a great idea. No, don't do that, please. That is a false economy. I have a better solution for you all. We're going to form just enough refactoring on our existing code base so that we can make the code testable. Then we're going to write tests for our code. Lather, rinse, repeat, and profit. We are at a PHP conference. I'm going to make a few assumptions. I'm assuming that you're PHP developers and that you have access to a terminal with Composer. I'm going to introduce a few tools. The main unit testing framework in the PHP ecosystem is PHP unit. I didn't update this slide, but I believe PHP unit nine is out now. There are older versions. Even if your PHP code base happens to be a legacy, 5.6, there are still versions that you can use. You can still go and implement the techniques that I'm going to be explaining today. It's really easy to install if you have Composer. You want to do Composer require dash dash dev because you don't need unit testing production. PHP unit, PHP unit. Then you can test that it works using vendor bin PHP unit dash dash version. I should give you the output display. A unit test. For people that haven't done much unit testing before, I'll briefly explain what a unit test looks like. A unit test is usually a class that extends the PHP unit base test case. Inside this test class, we have a group of functions, and each function is a test. You usually have, for example, a class, a test class that is testing one of your classes in your actual application, and then each function is testing each different behaviour of that class. In this example, we have example test that has one test in it, and it's to test the output or the result of some function. Within our test function, we're running the thing that we want to test. That's the underlined sum function. We want to assert that when we do run that function, it returns the value of true. If we were to run this test, hopefully we would get the correct output. To run our tests, and I've put all of my tests in a directory called tests, so I'm doing vendor bin PHP unit tests to tell it where my tests are, and it should run it, and you get a nice output. It's a little dot to say, yep, the test pass, everything's working. Great. There's another thing that I want to introduce, and that it's mocking. Mocking is really useful in unit testing. It enables you to create test doubles. If you're testing one particular part of your application, it probably is going to depend on other parts of your application because nothing really exists in a vacuum. You don't necessarily want to use real objects in your tests. It might have unintended consequences, or behave strangely, or just be overly complex. Mocking allows you to create test versions of objects in your code. It helps you to verify the behavior, regardless of what that test object is doing, and isolates what it is exactly that you're trying to test. A bit of a caution when it comes to mocking, it's nice to mock dependencies when you need to, but try not to mock everything, because your code does not exist in a vacuum, and sometimes you mock things and think the tests pass, only because you really told them to pass, and not because they actually would in a real-world scenario, and that's the warning about tautological testing, which often happens. If you were to mock the system under test, please under no circumstances, no matter how tempting it is, do this, because then you're not really testing anything. So PHP unit does have some mocking abilities built in already, but I prefer to use this library mockery. I find it easy to understand and to use, and you can install it again using Composer. Composer require dash dash dev mockery mockery, because hopefully you're not creating mock objects in your application code, and this is how you would create a test object using mockery. You do mockery, colon, colon, mock, and then pass it in the name of the class that you want to mock, and it will create a version of that class with the correct types, so it will pass all your type printing, but it is actually just a dummy. And you can perform expectations on your mock objects. So in this instance, we're saying that at some point during the execution of our code, this mock object should receive a call to the method method. It should only receive a call to that method once, and when it does, it should always return the value true. This is not asserting on the return value of that method. We are telling it that it will return true, and that allows us to essentially force the flow of our application. When you're using mockery, you have to remember to close it. This is just a sort of side thing. If you don't close your mockery after you've done it, it won't count the number of times that things have been called, and so you'll get failed tests. So just something to bear in mind. Just a disclaimer. The following examples are going to be deliberately simple. You're probably never going to see anything like this in your real application code. I've made them simple so that hopefully you can learn to identify the patterns that are going to be problematic when it comes to legacy code, and when you've learned to identify them, you can apply the techniques that I will explain to you to tame your legacy code. So I'm going to go through a bunch of different scenarios. Lacy code. So the first tricky testing scenario that we're going to cover today is interfering output. And when I say interfering output, I mean something like an echo or a print in your code. So here we have a function yodol, that if you pass it a tune, it will echo that tune back to you, otherwise it will just start its own little yodol. Great. So how are we going to test this? There are two possible branches to this. We can either test what happens when we don't give it something to echo, and we can test what should happen when we do give it something to echo. So we've created our new unit testing class, given it a name, and we've got our two methods in there. Each one is a different unit that it's testing. And in the first one we're going to test our yodol without being past any parameters. And in the second one we're going to test the echo yodol. Sounds great. We're going to run these tests and something weird happens. The output is actually in our test rather. There's also another thing to note here, is that it says there were two risky tests. Because if you notice in our example, we're not actually testing anything. We're just executing these methods. Nothing's actually being asserted. So that's why it's considered risky. So how do we deal with this kind of scenario? And just imagine for a moment that this you might encounter in an old application where you haven't really separated concerns. So for example, if you've got a render function, that ends up printing an entire HTML page, and then you get that in your test runner and it's very confusing and long. Anyway, first off, we're going to prevent that from happening. We're going to stop our output being vomited out into our test runner starters. We're going to try and cap to that output so that we can actually perform assertions on our captured output. So PHP has the output buffer, which we can start up using ob underscore start. And anything executed after this point, anything that is printed or echoed will be captured by the output buffer. And then we can sandwich it between ob end clean, which is where it will say, okay, we're not interested in anything that's outputted after this point. And to get the contents of our output buffer, we're going to use ob get contents. And we're going to assign that to our variable output. And now we have something that we can actually assert on because we've got a variable with some data in it. So we can use PHP in it and say this assert equals only an output and our tests pass. Similarly, we can do the same thing for our other branch, our alternative branch, where we are passing it a yield to echo back at us. We can assert on that value, and it is also successful and passes. Another thing, the second thing that you're probably likely to encounter in your legacy code is static method calls. There are some reasons that people don't like static methods and the difficulty to test them is probably one of them. So anyway, we are Vegeta, and we have a scanner, and we've seen Goku, and we want to see what his power level is. And if his power level is over 9000, we're going to be like, it's over 9000! Otherwise it's like, okay, he's suppressed one. So this is our method, our get power level method that gets the result of Goku's power level and returns the correct response. So again, we're going to create our test class, and we've got two possible scenarios here. Either Goku's power level is over 9000, or it's not. So we're going to call power level, and we're going to assert on the result of that. If we actually look at Goku, the static power level method on Goku is always going to return 5000. Which means that this is fine, because 5000 is under 9000, so he's always going to return 5000 and it's going to be suppressed. But if we want to test the branch of it's over 9000, then it's not going to work, because it's never going to be over 9000. So how do we deal with this kind of scenario? We're going to create a new mock Goku, using mockery aliasing. I'll explain that in a minute. We're going to force the return value of Goku's power level to be 9001, which is indeed over 9000. And we're going to have to run our test in separate processes. It's kind of a weird thing I'll explain in a minute. So we're going to create our Super Saiyan Goku using mockery mock, alias, colon and then Goku's class name. The problem with aliasing is that it completely overrides the classes that already exist in your application. So you can only create an alias if your class hasn't already been loaded, for example via autoloading, which is why you can't use the colon, colon, class magic thing. You actually have to type the fully qualified name in the alias. So here, we have a Super Saiyan Goku. We're going to say that at some point during the execution of this code it's going to receive a call to power level and when it does, it's going to return 9001. So yeah, now our test is successful. So we've tested our two branches. So yeah, a note about using alias, because it does override the class that's in your application, it's kind of not recommended. So yeah, this is from the mockery documentation. It clearly says that even though it is supported that you probably shouldn't use it. So there are a couple of other ways to solve this. But the problem that it's going to present you is that when you run your tests it's probably going to complain that because you have some tests later that want to use the actual real class and not the class that you created that's a mock. So you're going to complain oh, this class already exists, you can't do anything. So what you'll end up doing is having to run the tests in your mocked class in a separate process and disable the global state so that it forgets that you've mocked the class after it runs this test suite. Which is fine, it works, but it's very, very slow. So not great. But if this gets you one step closer to testing your legacy code then it's all good, right? The third thing that we're going to consider today is hardcoded dependencies. Such as this. We're in Britain, I'm British and my outlook on life depends a lot on what the weather is like which is to be fair, mainly grey and horrible. So I have a method here getOutlook which creates a new weather forecast and it looks like a weather forecast and if it's sunny then it fills up my glass half full. Great. Otherwise my glass feels like it's half empty and I'm kind of a pessimist and it's going to return the value the glass is half empty or full depending on what the weather forecast is. Great. The problem is my weather forecast is just about as good as any real weather forecast and that it's pretty random. Sometimes it's sunny, sometimes it's not, sometimes it's correct, sometimes it's not. And yeah, our sunny method returns a random value. Great. My two scenarios, whether our glass is half full or a glass is half empty, we need to get the result of our outlook and assert on that result but half the time these tests are going to pass and half the time they're going to fail. Which isn't great because we kind of want our test to be a bit consistent. So yeah. We're going to look again to mockery and we're going to mock our weather forecast using a mockery override this time not alias. We're going to force the return value of is sunny to be true or false depending on what branch that we want to test and we can perform assertions based on those mocked return values and again we're going to have to look at running our tests in separate processes because mockery override intercepts a class on creation. So you would use an override for non-static methods when you're creating a hard coded dependency on the fly but you would use alias for static methods because alias creates a whole new class and override intercepts an existing class and turns it into a mock. So here's mockery coming we're going to use overload weather forecast you can use the magic colon class thing in this you don't have to type out the fully qualified name because it's an intercept rather than the creation of a class we're going to say that at some point it should receive a call to is sunny and when it does it should return true and in that case our glass should be half full great that works and again the other way around if we want to test the other branch we're going to say oh it's going to return false and our glass is half empty great but again you're going to probably find that you're going to have to run your tests in a separate process because there are probably other tests in your suite that are testing the forecast that you don't want to be mocked so in order for them not to all be mocked you need to run the tests in a separate process just fine it gets you moving on the way towards testing great cool but it's slow so there's a better way we can refactor this so we're going to refactor the code to use an injected dependency we're going to mock that injected dependency and we're going to inject the mocked dependency and then we can remove that annotation so who here has heard of dependency injection almost everyone okay who can confidently explain what dependency injection is few of you right so I'm going to demystify this for you here we have our get outlook and our new weather forecast this is a hard coded dependency because we are creating the dependency within our actual method instead we can inject our dependency using dependency injection that is creating a dependency outside of our method and passing it in parameter and that is the essence of dependency injection dependency injected now you know so in this case this is a safe refactor because I've given it a default parameter value of null and then in our next line when I'm getting the weather forecast I can either use the dependency that has been injected or I can do what it was doing already anyway so the behavior hasn't actually changed we can just create a new weather forecast on the fly if one is not being passed this means you don't need to go through your whole application and change all of the signatures to this method it just works and we've got our dependency injection in place so in our tests it means we can create a mock of our weather forecast we don't need to use overload anymore we can just give it the class name and create a whole new mocked class and that leaves all the other creations of this class untouched which is great we're going to say it should receive a sunny and return true and we're going to inject that dependency into our get out look method and hooray this works and we don't need this anymore cool that's solved there's another thing that you commonly encounter in legacy code and those are ungraceful exits the first kind of ungraceful exit I want to explain and demonstrate today is the redirect so you're the nicest around here and you want to enter the cave of Kabanog but there is a rabbit killer rabbit or there may or may not be a killer rabbit so if you do encounter a killer rabbit you're going to run away but if you don't then cool behold we're going to enter the cave of Kabanog so again we have our two branches to this you can either enter the cave safely there's no killer rabbit you're good or there is a killer rabbit there and you're going to have to run away you're going to have to redirect but when you run these tests or when you run these methods in these tests you're going to quickly find that everything goes a bit weird because there's a header change in there because the redirect is setting a header and you need to run it in a separate process just so that it doesn't confuse the entire test suite so that's one thing so when we try to enter our cave of Kabanog safely and there's no rabbit there it's pretty simple because nothing redirects us we just continue with the execution in the function and behold it returns correctly but we want to assert that when there is a killer rabbit in a way that we don't get behold but the odd thing is in this scenario that we do that's because in a test runner we don't have any headers or redirection or things like that in place so if we go back to our method it gets to the killer rabbit it goes okay I'm going to redirect but it doesn't because it's in a console it's a test runner it's not going to do and then it continues with the execution and it returns behold still this wouldn't happen if you were running this in a browser because you would get redirected and the execution would stop but in a test runner it does so we need to remember that if we're anticipating that a user is going to be redirected or the execution is going to stop that we actually make the execution stop that is to put return or continue or break or whatever it is to exit the execution of your method after redirecting or you could technically get the header and put it right at the end of your methods so that there's nothing that it could potentially do after that point so this is a safe refactor in that the actual behaviour of the method hasn't changed because after you've been redirected it would have stopped execution anyway so this is a way that you can make your tests work and everything is sane or the other way around return early so the header is the very last step and you don't continue executing something and get unintended consequences cool and we're asserting that when there is a killer habit in the way that we don't get behold we don't get thrown to the cave and we run away there's another type of ungrateful exit you know what's coming so we have Brexit we have our Brexit method and if we're a remainder we're going to say bollocks to Brexit and if we're not we're going to exit so we want to test our two branches of method either we're a Brexiter and the result is that we're going to exit we're going to crash and burn and everything's going to be horrible or we're a remainder and everything will be fine so in our test remainder function we have a Brexit our remainder is true and we're going to assert that the result of that function is that it says bollocks to Brexit cool successful but when we're a Brexiter and we run this method and we want to assert the result is empty it never gets there because exit does completely exit whatever your execution path is it completely exits the test runner so we need to think about refactoring this so that we can get this tested I like to create a wrapper around that exit functionality to remove it from the method and then we can inject that wrapper and mock it so that we're going to create sort of a fake Brexit with a bit of safe refactoring using our magical dependency injection so here's our wrapper around Brexit it's Boris and he's going to get Brexit done and he's going to completely exit cool and we have our Brexit method and we're going to pass in our Boris dependency and again we're going to give it a default value of null because this is a safe refactor we won't need to change any of our calls to Brexit because it will use a null value and if the value is null it will go and create a new Boris on the fly so yeah you don't have to change any of your existing code and instead of exiting we're just going to be Boris and we're going to get Brexit done so cool in our test Brexit method we're going to create a mock of Boris which is always fun and we're just going to make him do nothing he's just going to sit there and be silly and then we're going to well we've got to make sure that our first parameter to Brexit remainer is false and we're going to pass in the mocked Boris and yay this works don't try and unit test Boris because all he does is exit and it would be kind of pointless and I know that means that you're not going to have 100% coverage but it's just kind of a trade off that you have to make when it comes to testing legacy code this is another thing that you're going to encounter probably in legacy code and in code in general to be fair interaction with the file system so we're going to take a trip back to the 90s remember we had all those web pages on like geosities and they had all the animated gifs and stuff like that and also a hit counter because it was very important to show people how many people that actually visited your website so this is our hit counter and our hit counter method which counts hit and we have a hit counter file we're going to read the contents of that hit counter file and then we're going to increment it by one and save that back cool we want to test this so we test count hit we need to find the contents of that file before we count a hit so that's where we're getting our count before we're going to count the hit we're going to get the contents again and then assert that the value after a hit has been counted is equal to one plus the count prior to that but there's some problems with this because you're artificially inflating your hit count and that would be frowned upon because every time you run your test you're actually incrementing the value in that file so that gets kind of annoying and interfering and it just looks like we're more popular than you actually are so we're going to have to refactor this as well and again we're going to return to our trusty dependency injection and instead of hard coding the file name which is a dependency we're going to inject that and we're going to create a test double hit counter file and we're going to reset the contents of that file after each test just to keep it clean and make sure you don't end up with a file with a gazillion hits just a bit simpler so here is our dependency injection at work again we have a file parameter and we're going to give it a default value equal to the value that was already in the file so that we can either give it a new file name to use for example a test file or just use the file name that was already in there anyway cool so instead of using our actual hit counter file in our tests we're going to use testhitcounter.txt testversion of our hit counter we're going to inject that dependency and everything works in our teardown we're going to do just file put contents and reset our counter to zero in theory we could just assert previously that the count would always be one after running this method because if it has a zero and it's begin with then resetting the contents after each test run then it's always going to be one but there's a better way because with this you're maintaining a test hit counter file which kind of clutters things up it's a bit messy you have to make sure that's committed and make sure no one chooses the value of it so yeah there's another way and I'm going to introduce a tool today called vfsstream which creates a fake or virtual file system so instead of doing this testhitcounter file we can create a new virtual file stream using vfsstream and then we can create a new hit counter file using vfsstream new file give it a file name tell it where it wants to put it which is in the virtual file stream and we're going to set the contents of that file and then instead of a string file name we can pass it hit counter file URL which behaves in the same way file name to any other file on the system but it's virtual and we can always assert that it's going to equal one after we've counted a hit because it's setting the contents of zero before each test run so yeah that's quite handy we can just get rid of this stuff now we don't need it makes things a bit simpler everyone has to forget you right just maybe not in your code base so yes because you code that's the thing that you're going to encounter okay so I have a bolognase and for some reason well okay I have a bolognase it can be either eaten or not my place for is not I can eat the bolognase and I can check whether or not the bolognase has already been eaten and it also has palms and and looking into our eat method if we've already eaten a bolognase we can't eat it again obviously that would be weird if it doesn't have parmesan cheese on it or if I've eaten the parmesan cheese on it I want more parmesan on my bolognase because I quite like parmesan and it doesn't taste right without it but also I need to have garlic bread for my bolognase because it's not bolognase with a garlic bread right okay so I've got to have the garlic bread and if I'm eating the bolognase I'm going to eat the garlic bread as well and if I'm eating the bolognase I'm going to eat the parmesan as well and then it's going to say eating to be true this is a bit weird because it's spaghetti and I'm not sure why spaghetti is concerned about whether or not I'm also eating garlic bread or parmesan cheese to be fair so yeah this is literal spaghetti code there are lots of dependencies in there some kind of intermingle no one really knows what's going on no one really knows what to expect so if we want to test something like this we create our bolognase test file and we want to test what happens when we eat the bolognase so we're going to create any bolognase and we're going to eat it and yeah this works we're going to assert that after we've eaten our bolognase that he is indeed eating but there are a few tricky things about this because when you test something like that it does execute all of the code in there so it's going to look like in your test coverage that we've also covered a bit of parmesan and a bit of garlic bread which is a bit misleading so there's a covers annotation in PHP unit which you can use to specify what it is exactly that you intended to test so when it comes to spaghetti code you can tell it that actually although you've executed this function which has had a ton of side effects because everything's tangled up that I had only intended to test the bolognase and it will discard the coverage for everything else that it's come into contact with I also like to create reusable expectations so for example I might illustrate different process branches or scenarios that occur in multiple times or I want to test multiple times in spaghetti code I'm trying to explain so yeah I've added the handy covers annotation so when I eat the bolognase I'm telling it that I only actually intended to test the bolognase class and not everything else that seems to come with it and I want to test now that when I try to eat a bolognase it's already been eaten that it can't be eaten again and I also want to test that when I eat a bolognase that hasn't already been eaten that the correct behaviors occur so here are two tests bolognase that's already been eaten and one that hasn't when I run these tests I kind of want to check that when I'm eating a bolognase that hasn't been eaten that I also eat garlic bread so I've created a private function in my unit test class which is expect garlic bread to be eaten which creates a mock of garlic bread and overload because it's a hard coded dependency and I'm going to say that if I am garlic bread should be eaten so that I can use this expect garlic bread to be eaten elsewhere in my test so when I'm eating a bolognase that hasn't yet been eaten I don't want to expect to eat garlic bread but when I am eating an uneaten bolognase I do expect to also be eating garlic bread and that's just sort of one way of taming this spaghetti code whilst you attempt to refactor it a bit another thing that you're likely to encounter in any code is the private method so I have MC Hammer here and when you say stop he's going to say can't touch this and that's going to return hammer time so cool we can test can't touch this we're going to create a new MC Hammer we're going to call can't touch this and when that happens we're going to assert that it returns the result of hammer time but we can't we get an error because can't touch this it's a private method so how are we supposed to test this well don't just leave it test the public methods that use the private methods so they get tested by proxy that counts that's absolutely fine private methods are as they say private they are implementation details you shouldn't really care about them anyway so yeah feel free to ignore those so instead we're going to test stop and indeed that calls can't touch this which indeed returns hammer time and our tests are successful and there are no errors great so to recap I've explained a bunch of scenarios that you're likely to encounter in your work with legacy code hopefully you will be able to recognise some of these patterns and understand some of the techniques that you can employ to get these tested and once you've got them tested you can refactor happily safely nothing's going to go wrong legacy code is not something that you should fear or need to fear legacy code is an asset legacy code is what you are being paid to maintain and you didn't have to write it and now hopefully you understand you don't need to rewrite it either thank you