Table of Contents
Video 17: Testing, Part 2: Writing Unit Tests
The seventeenth VuFind® instructional video dives deeper into testing, showing you how to build a real PHPUnit test to improve VuFind®'s test coverage.
Video is available as an mp4 download or through YouTube.
This is a machine-generated transcript and will be corrected as time permits.
hello and welcome to this month's viewfind video which is continuing the topic of testing which was introduced last time around uh in the previous video i showed you how to set up a test environment and actually run some tests and this video is going to assume that you have already done that and know how so this time around we're actually going to look into writing unit tests so first of all a quick reminder on what i mean about a unit test there are several kinds of tests that we can use in viewfind and unit tests are designed to test individual units of code so in theory every class in viewfind should have a corresponding unit test that confirms that it works correctly and you might ask why write tests there are a few reasons one is that test driven development is a very popular way of writing code in tdd you write your test first which specifies the behavior that you expect to see and then you write and revise your code until the test passes this is a really good way to get right at the programming portion of the problem and to ensure that all the code you write is doing what you want it to do of course some types of code are better suited to this type of development than others but certainly if you're writing a new component that does well-defined work doing it in a test-driven way is a great place to start uh and sort of a side effect of this development practice is that your tests become part of the documentation of the code you know it's it's certainly good to write documentation and to include good comments in your code but if you also have a test case that exercises the code and explains why it's doing that that helps to add to the understanding of what the code is for and what it's meant to do which can be really useful when onboarding new developers to the project and of course testing is important for some really obvious reasons we want our code to work correctly and if we have test coverage we can make changes with more confidence so for example if we want to refactor some code or make something more efficient if the code we're changing is completely covered in tests then when we make our changes if the tests still pass we can be reasonably confident that we improve the code without breaking anything now while i said in theory every class in viewfind should have a corresponding test unfortunately in reality that's not the case not all of this code has been developed in a test-driven way and so we actually have some pretty substantial gaps in our unit test coverage uh part of the reason i'm recording this video is that i'm hoping that some of you can help close some of those gaps and that you can be aware of this practice when making new code so that the existing shortcomings of viewfind's test suite don't become larger so let me start by showing you a useful tool for looking at where we have and do not have coverage of code with tests in viewfind if you go to viewfinds jenkins instance our continuous integration system uh and you go into the viewfind project uh you'll see that there is an option here that says open clover html coverage report and this will show us a report of all of the unit test coverage within the project i should also note that if you ever come here and this link is missing it probably means that a build is in progress and you can actually go to one of the completed builds down here to find the link to the report for that particular build but at this moment in time there's no build running so we can just get the latest report through here and when you look at this report uh you can see a lot of red so you know we have more work to do but uh the important thing about this report is that it lets us drill down into different parts of the code base at the top level here it's split up across viewfind's existing code modules and we can drill down into namespaces from there and it reports on three types of coverage it tells us how many lines of code are covered by tests so if a line of code executes while a test is running it is considered to be covered then there's functions and methods a function or method is considered to be covered if every line of that function or method is covered so our line percentage is going to be better than our function and method method percentage and finally classes and traits and again a class or trait is considered to be covered if 100 of its functions and methods are covered so again these numbers are going to be even smaller but uh let's just pick something that could use a little more coverage and that will give us an excuse to look at how tests are written and then make some improvements so i'm going to go into the main viewfinder module which is where most of viewfinder's code lives and as you can see that's currently about 25 covered so we have quite a bit of work to do so when i click into here i'm now exploring the directory structure of viewfinds code so there's a source directory inside the viewfind module i'll click into there i also need to click into the viewfind directory where the viewfinder namespace lives and now i've gotten to some things that are actually interesting so here are all of the sub namespaces within the viewfind namespace and i can drill down into any of those but rather than go too deep into the code i'm just going to come down here to the bottom where i notice you know we have a number of things that are green that have pretty good coverage a number of things that are red and have no coverage at all but we also have this export.php which has yellow coverage it's almost there it just needs a little more work so i thought this would be a good example to look at today maybe we can turn one yellow light green in the process of learning about testing so when i click through to an actual php file my report looks a little bit different i have a summary at the top showing all of the methods within the file and again these have the same you know coverage percentages and so we can see here that most of the methods are fully covered but we have a handful that are either not covered at all or are only partially covered and if we scroll down we can see that this report gives us all the source code and it colors in the lines based on whether or not they have test coverage which is really handy so this lets us get directly to the place where some work is needed i should also notice you'll see some yellow lines um and that just means that these are never going to be covered because the code returns here so it's never going to get to this closing grace and that's safe to ignore uh in in this particular instance and also you'll see that there are many lines which are not color coded at all and that's because those are non-executable lines of code you know comments don't count for code coverage uh function signatures aren't covered uh it's just the the contents that matter so let's start uh by taking a look at the method that is partially covered which is this get bulk export type method and as you can see we've got one red line in here and two green lines so that clearly means that what's happening here is that this first if condition is never getting matched in any of the tests which means that this return is never getting called so we are going to need to add a test uh to cover that edge case and then this whole thing will light up green and before we do that let's just uh step back a moment and talk about what this export php file actually is viewfind has the ability to export data to external bibliographic management systems and to a variety of data formats uh and that requires a lot of configuration because there are different ways different formats need to be exported and this export class is mostly just a wrapper around all the configuration that allows other code to figure out how to behave when exporting different formats of data that makes it a pretty good candidate for writing tests on because we have well-defined inputs and outputs that we want to test so hopefully writing these tests will not be too hard as you can see we have a couple of properties in play here export config and name config and these are just objects representing some of you finds any configuration files so main config is config.ini export config is export.ini and then all this behavior here is based on what settings are set in those configuration files all this should be easy to simulate in a test but before we write any code let's take a look at the existing test and how that works so all of view finds tests are contained within the modules where the code that the tests operate on live so we're working now in the viewfinder module so as i've showed in past videos all of the source code for the viewfind module lives under the source directory but there's also this parallel test directory where the tests live so let's open up the test directory and in here there are a few subdirectories there is a fixtures directory which is where we store test fixtures or data that is used by some of the tests uh we're not going to be looking at that today but it's good to know that that exists then we have integration tests which we'll be talking about more in the future and then the unit tests directory which is where the unit tests live so we're going to drill down into that and so inside there we have sort of a parallel structure to the name source code but we use the viewfind test namespace instead of the viewfind namespace just to prevent collisions the test framework doesn't care too much about namespaces but it's still helpful to use them for for that uniqueness and if i scroll down far enough here is my exporttest.php now as i mentioned in last month's video we use the php unit framework for testing and this is widely used and well documented and today's video is not meant to be a replacement for a good tutorial on php unit i would strongly recommend that you seek one out uh but i'm going to show you just enough to get a basic understanding of what we're doing here and hopefully to help you get started and then other tutorials can build upon that so every test has to extend php unit framework test case which is just the base class for test cases in the php unit framework any test case class then needs to have public methods whose names begin with the word test so every function in this class that starts with test is one of the tests that we want to run and as a general rule you want to make these pretty granular so that if the test fails you understand exactly what behavior didn't work correctly you could just make one big test function and put all your logic in there but then if something goes wrong it's going to be harder to figure out what went wrong uh and you know there's there's some art and some science to this but as a general rule i recommend erring on the side of granular tests so that you can identify points of failure uh and within each test the real heart of a php unit is making assertions because the idea is the test is going to try to do something and then it's going to make assertions about what it expects should have happened uh and a lot of these assertions are just you know calling a function and asserting that its return value will be equal to something so for example in this get bulk options legacy uh test we are running the exporter with a particular configuration and saying that when we call this method it's going to return this array and so php unit will pass the test if all the assertions are actually matched and it will fail the test if any of the assertions are not the framework has a pretty rich variety of assertions that you can make though much of the time you're going to be asserting equality or asserting that some condition is true but i highly recommend taking a look at the php unit documentation about assertions just to get a feel for all of the things that you can express in this way as some of them are quite interesting in any case if we scroll back down through this file we're going to see a whole bunch of tests all of which contain assertions because a test with no assertions is completely useless and then down below all the tests there are a couple of helper methods so i have this get fake mark xml that one of the tests uses to create xml records uh and i have this get export which is just a convenient way to create an export object for us to test as i mentioned when i was talking about the export class it relies on objects representing configuration files and so this method just makes it really convenient to pass test configurations into an object without having to instantiate the config objects or or do extra work and if we want to test the default behaviors the fact that these configuration parameters default to empty arrays means that we can just say get export and we'll get an all default object without having to do any configuration at all i usually find that when writing a test like this having some kind of a helper method to construct the subject of your test is really handy but anyway now that we've gotten a quick look at uh what a test class looks like let's start building on this so as we looked at before the get bulk export type method is not fully covered and if we looked closely in this class we'd actually see that there is no method explicitly testing uh get bulk export type the only reason that we're getting coverage on that method at all is that one of the other methods is calling it and that coverage is happening as a side effect so we're just going to want to create a whole new test method that tests all the functionality of get bulk export type so that we can be confident that that method is behaving the way that it's supposed to so when writing a test the first thing to do is to analyze the code and figure out what it's supposed to be doing so that we know what cases really need to be tested so let's take a quick look at this get bulk export type method again so essentially there are three things that can happen here the first thing that could happen is if the format that we're asking about has a section in export.ini and that section contains a bulk export type value we're going to return that value so this is the most specific case where the export format has a particular value configured for it if that doesn't happen the next thing that we're going to do is look in the main config.ini where we can set a default export type that will be used for everything that's not more specifically defined so if that value is set we'll return it but we use null coalescing here so that if this is unset we then return the default value of link so our test should really look at three things it should confirm that the default value comes back as link if we don't provide any configuration it should determine that if we ask for a format that doesn't have its own settings we get whatever the default configuration is and it should return the appropriate format specific setting if one is defined so let's write a test that does all of those things so i'm just going to go beneath the last test in the file since i'm adding something new here and i'm going to put on my comment test get bulk export type and all test methods have a void return the test framework is working off of assertions it doesn't care what values the functions give it so we will create public function test get bulk export and that will return a void so how do we want to do this first of all let's do the easy part we should get link as the default if nothing is configured so we'll put in a comment to clarify that and we can just do this directly with an assertion we're going to say assert equal and this is of course a helper method given to us by the php unit framework that we're inheriting from uh and in assertions you always put the expected value before the calculated value uh this is how you ensure that error messages are formatted correctly so it's always expectation then calculated values so in this case we expect to get link back uh if we call this method on a configuration free export object then we just need to call the method on a configuration free export object which as i demonstrated earlier we can say this get export to get that object with no configuration set and that will call the helper method i showed earlier and so then on that object we've created we can just directly call getbulk export type and we have to give this an export format uh and it's fairly widely used convention uh in tests to use values like who bar and baz when you're putting in an arbitrary value so i'll just request the export type of foo so here we're saying if we get an export object with no specific configuration and we ask for a bulk export type we're going to get link back because that's the default we expect that simple uh and you know it seems like this is kind of an almost pointless thing to test but it's important to test every edge case because you know say for example the code mistakenly assumed that there would always be configuration set having this test will confirm that that doesn't throw notices or cause unexpected errors always test every edge so that's our our first test and our easiest one for our other two cases the default configuration or the format specific configuration we have to do a little bit more setup work to make that work so first as always i like to start my test cases by putting in a comment about what i'm going to do before i do it so we're going to say we should get export specific values from export.ini if set otherwise we should get the default setting from config.ini so let me set up some configurations that we can use to test this scenario so first of all let's create our main config which should have a bulk export section which could that which should contain a default type setting and we'll just set that to download for this example there are three bulk export types that are currently supported which are link download and post since link is the default we can use the other two non-default values for our other two test cases which then lets us test everything as elegantly as possible we could also use fake values here and that would be equally valid for testing but in this instance it just feels right to use real values so that's what i'm doing so we've now set a global default bulk export type let's also create a format specific one so we can test both of these cases so the export configuration is keyed by the format identifiers so let's continue using foo as one of our examples let's say that who has a bulk export type of post so we've now created these arrays representing the configurations so we can pass them to our get export method to get a configured instance of the export object that uses these configurations so now our object is configured so we just need to make a couple of assertions so first of all we want to assert that if we ask now for the export type of foo we're going to get post back because that is what we configured in the export configuration so we can just assert equals post export get full export type of foo and now we need one more assertion to test the default behavior so for this we're going to assert equals download if we ask for export get bulk export type of bar so as i say foo and bar are popular placeholder values and tests we configured foo so we know what that's going to give us bar is not configured anywhere so we expect that to give us the default value of download and so now in a few lines of code we've tested all three of the edge cases that the get bulk export type method could give us and as i say this is a place where granularity is debatable we could really have three tests here each testing one of the edge cases and then if something failed it would be really clear which test had failed um but in this case having three entire tests for testing closely related functionality just felt like unnecessary overhead so i did this as a single test there's no right or wrong answer you'll get a feel for it as you work with it more but anyway now that i've written this test uh why don't we run it and see if it passes so i'm going to go to the terminal and i'm now in my viewfinder directory that i set up last time for a test environment and as we talked about then i want to have make sure that my environment variables are set correctly so i created this test environment.sh script in my home directory to do that i'm just going to source that to be sure that all is set correctly and then i can use my uh home directory thing.sh script to run my tests with the php unit fast task and using the php unit underscore extra underscore grams property to pass an extra value to php unit in this instance the file name of the test that we want to run which we can type out here as viewfind home slash module slash view find slash tests slash unit test flash source export test dot php when i hit enter php unit runs really quickly and it tells me that it ran 12 tests with 21 assertions and just to make ourselves confident that this really did work let's just temporarily break this test and see what happens so i'm just going to change this assertion so that it now expects z-link instead of link i go back to the terminal and i repeat that test it now fails it tells me it expected z-link but it got link it gives me the exact line where the test failed this is all really useful stuff and it will help you tremendously if you're using this for test driven development so we've done it we've written our first new test case here all three edge cases are now covered and it looks like the code is working the way we expected it to work if i go back to the coverage report let's see what else needs to be done so at the bottom here we have a couple more fairly straightforward methods that are just wrapping around configuration and returning defaults so for example get post field is either going to give us the post field defined in the export.ini or it's going to give us a default of import data very similarly get target window is either going to give us the target window setting from export.ini or it's going to compose the format name with the word main to get a window name i will write tests for these but i'm not going to make you watch me do it because this is extremely similar to what we just did however there's also one more uncovered method that's significantly more complicated to cover and so let's take a closer look at that one and that is this get bulk url method and this is more complicated because this method interacts with another object uh in this instance it's accepting a php renderer object from the mvc view layer of the laminas framework and it's using some of the helpers in that object to do work specifically it's relying on the url helper to call the laminas router and it's relying on the server url helper to figure out an absolute url and as i mentioned unit testing is about testing one unit of code the export class it is not our job in this test to test the url helper the server url helper and the php renderer we really don't care about those things except the extent that they expose an interface that this class depends upon and so it is in cases like this where we need to use mock objects mock objects are a useful testing concept where you create a fake object that has predefined behaviors which stands in for a real object during a test and allows you to simulate interactions fortunately a php unit comes with built-in quite rich support for object mocking and so you can use that when you run into a situation like this uh to test behaviors without having to bring in and construct lots of external objects and dependencies obviously there's also a need to test that real interactions really work but that's what integration testing is for we'll talk about that later when we're unit testing we generally don't want to go down that road and mocks are preferable as with everything there may be exceptions where it actually makes sense to bring in another class but that is the exception rather than the rule and you should think about mocking first uh when you run into something like this so let's analyze this code a little bit before we start writing uh just to see exactly what it's doing so this is taking the the view object the php renderer it's taking the name of a format and it's taking an array of ids and what this method is used for is constructing a url that can be used to export multiple records in a single batch so this is used by the bulk export functionality so we just need to test that this constructs an appropriate url for the input variables uh and we don't really need to care too much about the internals of any of these helpers that it's using so what are we doing we're creating an f parameter that has the export format we're creating an array of i parameters that contain all of the record ids we're looking up the cart do export route which is going to do the actual work uh we're making an absolute url out of that so we're first using the url helper to turn this route name into a path and then we're using the server url helper to turn that path into a full url and then we're appending some parameters onto that base url based on this array we constructed up here then depending on whether or not the format we're using uh needs to be redirected to or can be directly accessed we're either using the get redirect url helper method or we're returning the url in an unmodified form so there are really kind of two paths we want to test here the needs redirect path and the does not need redirect path uh and in the interest of keeping this video short i'm just going to uh test the default behavior i'll do the rest of the work subsequently um so this is pretty straightforward we're just checking that a few things get plugged into a url it only gets complicated because of the use of this helper code and the need for mocking so let me show you how that works as before we are going to want to create a new test method which i'll put below the existing test methods we'll just say test get both url method returns void public function test get bulk url void all right so when you're doing mocking you sometimes have to kind of build things in an inside out way because what we have in this code is two different uh helper objects that are being retrieved from a container object there are a variety of ways we can approach this i'm just going to do this the most brute force way possible make two fake helpers that do fake work and a fake container that returns the fake helpers so i have to build the helpers first so that i can then put them inside the container so first of all let's just create our url helper and the way that you do this is there is a method that you inherit from the base test case class called get mock builder which uh gives you an object that can build mocks so i say this get mock builder and then i pass it the class name of the class that i want to mock in this case it's going to be laminas view helper url which is the view helper the url view helper class then uh it's usually a good idea to call disable original constructor which just tells the mock builder to build the mock without calling the real constructor of the real object which you rarely want to do when you're mocking and then i say get mock and that's it that's going to give me a mock url object which has the interface of the url view helper but doesn't actually do anything so there won't be any unexpected side effects of the test now uh when you're building mocks one of the most important things is to set up some expectations on those mocks uh as i said making assertions is the key to successful tests and when you're building mock objects you actually set up their assertions about what they are going to do or how they are going to be used which is how you verify that your test is behaving correctly so uh in the case of this particular object we're just expecting the object to get invoked so that it can look up a route uh and that's going to use the magic method underscore underscore invoke so let me show you what this looks like to set up an expectation uh you just call the expect method on the mock object so i'm going to say url expects and the expect method expects a parameter about the frequency or type of access of the method that you're going to be expecting so in this instance i expect my my code to call this method once and only once so i can say this once which is a helpful helper method that tells us expect one call there are some other things you can do here so for example i could say this any which means that it expects zero or more calls to the method it's really flexible but not very specific i could also say this exactly one which means that this will be called exactly once it's the same as saying this once uh just less concise so i'm going to put this back we expect this method to be called once now we actually need to tell it what method we're expecting so we uh arrow to method underscore underscore invoke now we need to tell the method how it's going to be called and the very nice with method is useful for this with allows you to make assertions about all the parameters of the method that you're expecting so in this case we expect invoke to be called with the name of a route cart dash view export so i can just say this equal to cart do export so we're now expecting that the method will be called with the parameter cart do export and if my invoke method had multiple parameters i could put a whole series of assertions here one for each parameter but in this case there's only one so i'm done and finally i can close by saying what i want my mock object to do when this method is called in this case i want it to return a value which i'm going to say is slash cart slash export so this is going to simulate what the real view helper does it takes a route name and it returns a path but it's going to do that without actually using any of that code because we don't want to be distracted by the internals of that view helper it should have its own test that's somebody else's concern we just want to plug in some behavior to test the subject of our test the export class now this whole mocking syntax can take some getting used to uh it uses what's called a blue-ins interface where methods return objects which expose additional methods um it doesn't necessarily look like a lot of php code that you've written but it leaves really nicely because i can actually say url expects once method invoke with the value equal to cart do export will return the value cart view export so this is actually quite readable in terms of expressing what the test is doing so that's in any case one uh method down but now we need to mock the other helper which is server url and again we're going to follow a very similar pattern here uh this get mock builder [Music] helper server url class disable original constructor get mock and again we need to set up our expectations for the server url helper this expect once the invoke method with and in this case we're going to expect the input of the server url helper to be the output of the url helper so we expect this to be equal to slash cart slash view export because that is what the url helper was defined to return up above and we will say this will return a value of http colon slash localhost slash cart slash view export and again this is all fake but it's simulating what the real view helper would do which is taking a path and prepend a full host name to it so we're getting them but we now have our helpers only we need to also simulate the php renderer container that the code is pulling these plugins out of so again it's a very similar pattern view equals this get mock builder this time we're going to be mocking laminas view renderer php renderer and of course we know this part by looking at the type hints in the function that we're testing which specified that the view object was one of these php renderers that's how you can sort of get into this and then of course if we weren't already familiar with how the php renderer works we might have to look at that code and get a better understanding of it in order to mock it correctly but in this instance i know how it works and so i don't want to go too deep into those weeds but anyway for mocking this we want to disable the original constructor as with all the others and then get them on object now the behavior we want to mock here is a little bit more complicated than the behavior that we are mocking for the individual plug-ins because in this instance the same method is getting called twice with different parameters and returning different values because first the plug-in method is called to get the url of rather the server url helper and then it's called again to get the url helper fortunately uh the framework gives us some helper methods that make this quite easy to do so we're gonna say you expect and this time we're going to use that exactly specifier i mentioned earlier we expect exactly two calls to the method plug-in because two plug-ins are going to get pulled out of here by the calling code and since we're going to have different things happening instead of using with and will we use a different pair of methods first we're going to say with consecutive which allows us to specify an array of parameters for a series of calls so our first call we expect is going to be server url and our second call is going to be just plain url so now we've defined two inputs then we can use will return on consecutive calls to define multiple return values so on the first call we want to get back the server url mock that we defined above and on the second we want to get back the url mod and that's it we set up our entire uh mock test environment we've created two mock view helpers that do very specific things and we've put them inside a mock container that expects a very specific thing of course this isn't entirely ideal because this test is actually capturing some details that don't matter from an implementation perspective for example the order in which the two plugins are pulled from the uh container doesn't impact the functionality of the code if we reverse the order in which the plugins were retrieved we would break this test even though it wouldn't impact the functionality that's not ideal but in the instance in in the interest of convenience we live with some of these sub-optimal things but it's it's the kind of thing you might want to keep in mind during your test design can we make this more general um but this is a really good way to demonstrate mocks so i'm doing it this way so with all of that done um we now just need to make an assertion so what i'm going to do is i'm going to assert that i'm going to get some kind of a url i i'll figure out what that url is going to be as i work through it all so for this test i'm just going to test the default configuration uh i'm not going to worry about passing in a special configuration yet um i would want to do that later to test that other case i talked about uh based on whether or not we're working with a redirecting url but for this test we'll just test the default behavior so i can directly call getbulk url on a default export object retrieve from the getexport helper i need to pass it as the first parameter my mock view object and the second parameter is the name of the export method i'm just going to use that placeholder of foo and the third parameter is an array of identifiers i'm just going to arbitrarily give that one two three so i happen to know uh what i should expect here so first of all because of passing because of the way i set up my mocks i know that the url i get is going to start with localhost slash cart slash view export because i've set up my mock server url method to return that and i'm also testing through my other mocks that you know this particular route is being requested of the url helper and so forth but if i get a url starting with localhost slash cart view export out of my get bulk url call i can be quite confident that my mocks were used correctly and that all of these assertions were met if anything was wrong along the way the test would have failed before reaching this point i also know based on reviewing the code earlier that i'm going to get an f equals foo parameter on here um because i passed in an export method of foo the code is adding that and then i'm also going to get three ids added onto here uh and they're going to be a little ugly because they're url encoded but that's going to look like i percent 5b percent 5d for those url url encoded brackets i'm going to have one and two and three and that's it if i've typed all of this correctly i'm setting up some mocks i'm making an assertion my test should pass let's see if i did it correctly so if i go back to the terminal i can see that when my test passed earlier i had 12 tests and 21 assertions i should now expect if i run my tests and they pass that these numbers will go up i should have 13 tests and significantly more assertions because all of those setups in my mocs are making more assertions let's see what we get we get a test failure because i made a typo so let me go back and fix that i if i have it i type https here when in fact i'm returning http here and i'll just pretend that i did that as an exercise so now if i run the test again this time it passes as i predicted i have 13 tests more assertions we're up to 25 now and so that's uh a lot of the basics of what you need to see to understand unit testing if you can make assertions and you can mock objects you're well on your way to writing all kinds of useful tests but let's not just end there because i happen to notice while i was writing these tests that some of the code in the export class is not quite as efficient as it could be so let's benefit from these tests by optimizing the code and then using the test to confirm that we didn't break anything so let me show you the couple of things that i noticed first of all in get bulk url we create an empty params array and then we append the value to it there's no reason to do this in two steps we can just initialize the array with the value and then we've trimmed off a whole line of code that we didn't need to have similarly down here when we were looking at get bulk export type this entire if statement is really not necessary because now that we have the lovely null coalescing operator we don't need to do all these is set checks anymore we can reduce this entire function to a single return statement with no ifs at all so i'll just consolidate these comments and then i can change this to just return the format specific type if it's defined otherwise null coalesce to the global default type otherwise null coalesce to the default of link we've just changed together the options in order of priority much much more readable code once you understand null coalescing we got rid of several lines of unnecessary code so now i just run my tests and they should still pass and they do so i've just refactored with confidence and that's all i have for now uh next time we will look at some integration testing uh in the meantime as i said i really encourage you to read a little more about php unit maybe consider uh trying to expand bluefind's test i'd love to get some pull requests adding new tests uh and if you have any questions or problems along the way just reach out to the usual support channels and someone will help you out happy testing talk to you next time