Warning: This page has not been updated in over over a year and may be outdated or deprecated.
videos:testing_2
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
videos:testing_2 [2021/05/07 16:23] – created demiankatz | videos:testing_2 [2021/10/22 14:36] (current) – [Video 17: Testing, Part 2: Writing Unit Tests] demiankatz | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Video 16: Testing, Part 2: Writing Unit Tests ====== | + | ====== Video 17: Testing, Part 2: Writing Unit Tests ====== |
- | The seventeenth | + | The seventeenth |
Video is available as an [[https:// | Video is available as an [[https:// | ||
Line 12: | Line 12: | ||
===== Transcript ===== | ===== Transcript ===== | ||
- | // Coming soon! // | + | // This is a machine-generated transcript and will be corrected as time permits. |
+ | |||
+ | hello and welcome to | ||
+ | this month' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | we use the php unit framework for | ||
+ | testing | ||
+ | and this is widely used and well | ||
+ | documented and today' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | going to get link back because that' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | that real interactions really work but | ||
+ | that's what integration testing is for | ||
+ | we'll talk about that later when we' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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' | ||
+ | because they' | ||
+ | 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' | ||
+ | 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' | ||
+ | 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 | ||
---- struct data ---- | ---- struct data ---- | ||
+ | properties.Page Owner : | ||
---- | ---- | ||
videos/testing_2.1620404601.txt.gz · Last modified: 2021/05/07 16:23 by demiankatz