· 9 years ago · Nov 15, 2016, 07:10 AM
1# Introduction
2- Until about a year ago, this was me... (testing code in production)
3- I was aware of testing tools, but rarely wrote tests.
4- Then I joined an eXtreme Programming team
5- This is us, pair programming. One of the core tenants of XP
6- Another is Test Driven Development or TDD
7- This means we write tests first. Then we write just enough code to make the test pass. Then we repeat the cycle.
8- It's funny, because I thought I wrote working code. TDD has shown me that's not always the case.
9- Now, I try to write tests for all my projects.
10- I don't always practice TDD and I don't always test everything.
11- So I'm not here to start any flame wars about *how* to write tests.
12- I want you to leave being able to write tests.
13- I'm going to show you a few of the popular testing frameworks.
14
15# PHPUnit
16- If you haven't done so yet, you'll want to clone my sample project
17- It contains all of the tools we'll be using today.
18- Let's start with PHPUnit
19- PHPUnit has been around for a while
20- It has excellent documentation and resources
21- You'll find *Unit-style* testing frameworks in other languages
22- Basically each test contains one of more assertions
23- What we're going to do is create a *Collection* class
24- It'll have methods like *isEmpty*, *size*, *add*, *remove*, and *contains*
25- For this first example, we're going to practice TDD.
26- Again, not to be dogmatic. More pragmatic, as it will demonstrate the testing process.
27- `composer install`
28- `vendor/bin/phpunit`
29- It's expected to get a failure. We have neither configured PHPUnit, nor written a test.
30- So, let's write our first test.
31- Create a file inside of the `tests` directory called `CollectionTest.php`
32 - If you are using PHPStorm, you can use the *PHPUnit* template from the *New File* menu.
33- All PHPUnit tests *extend* the `PHPUnit_Framework_TestCase` class
34- Now we can run PHPUnit again with our new test case.
35- `vendor/bin/phpunit tests/CollectionTest.php`
36- You should receive a warning stating that we don't have any tests.
37- *Where to start* can be the trickiest part of testing.
38- It's sometimes a little harder when practicing TDD.
39- We don't want to test everything with one test.
40- The goal is to find an entry point into our `Collection` class to help us get a foot in the door.
41- From there, we'll spider out to the next test.
42- In this case, I like to start with *isEmpty*.
43- It's a simple method and will ultimately drive us to create other methods like *size* and *add*.
44- So, let's write a test for *isEmpty*.
45- In our `CollectionTest`, we'll make a public method called `testIsEmpty`
46- By default, PHPUnit will run any public method in your *test class* with the `test` prefix.
47- So, let's new up a `Collection` object
48- Then we'll call `isEmpty` on it and assert that it returns `true`
49- `vendor/bin/phpunit tests/CollectionTest.php`
50- Oh noes, we got an error.
51- In fact, this is considered the first failing test.
52- If you were using PHPStorm or an editor with dependency resolution, you might have noticed this already.
53- We never created our `Collection` class.
54- Let's create a file named `Collection.php` inside the `src` directory
55- For now, we'll also simply `require` it at the top of our test.
56 - **Note** since PHPUnit is run from the root of our project, the path is relative to root
57- `vendor/bin/phpunit tests/CollectionTest.php`
58- Still an error, we don't have the *isEmpty* method.
59- Makes sense. We'll add it.
60- `vendor/bin/phpunit tests/CollectionTest.php`
61- Great, `null` is not `true`. By default, PHP returns `null` from a function or method.
62- So, to honor TDD, what's the simplest thing we could do to make our test pass.
63- Great, we'll simply `return true`
64- `vendor/bin/phpunit tests/CollectionTest.php`
65- And the test passes. Yay!
66- Are we done?
67- Right, let's write another assertion for a non-empty collection
68- Although we need to *add* something to the collection, we're not going to focus on `add` right now.
69- `vendor/bin/phpunit tests/CollectionTest.php`
70- Oops. We forgot to make the `add` method. We'll just make an empty one to match the signature.
71- `vendor/bin/phpunit tests/CollectionTest.php`
72- So, our original test still passes and our new test fails.
73- How can we make it pass? The simplest thing.
74- Sure, let's just create a simple state variable to determine if anything has been added.
75- `vendor/bin/phpunit tests/CollectionTest.php`
76- Yay! We're passing again.
77- So, are we okay with `isEmpty()`?
78- Ok. What's next `size()`, `add()`, `remove()`, `contains`?
79- `size()` is a good candidate as we already have the foundations to track state.
80- So, let's go ahead and write the full test for `size()`
81- Again, we'll add a `public` method to our test called `testSize`
82- Let's again create an empty and non-empty collection.
83- Here we'll use `assertEquals` to test that our expected size is what's actually returned by `size`.
84- `vendor/bin/phpunit tests/CollectionTest.php`
85- Of course, we need to create the `size` method in our `Collection` class
86- `vendor/bin/phpunit tests/CollectionTest.php`
87- Great, still failing, but a better message
88- So, how can we make it pass.
89- Yeah, let's introduce a `$size` property and track it like we have `empty`.
90- `vendor/bin/phpunit tests/CollectionTest.php`
91- Yay! All passing again.
92- Now, before we move on, let's take this opportunity to *refactor*.
93- This is an important part of *testing*. In fact, in TDD, this is known as the *blue* phase.
94- It exists, because we might have done a few things to get the test to pass. But now, being wiser in the future, there might be a better way to do so.
95- With the tests there to keep us in line, we are now free to *refactor* the code confident we will not change the existing behavior.
96- First, what do we think about these two properties...
97 - I agree. Let's remove `empty` and just use `size`.
98- `vendor/bin/phpunit tests/CollectionTest.php`
99- Great, everything is still passing.
100- It's also equally important to refactor our test.
101- Anything we see in our test that could be cleaned up?
102- We're doing a lot of the same setup in both of our test.
103- Let's combine this as part of our test setup.
104- PHPUnit provides a `setUp` method that is called before each test is run.
105- `vendor/bin/phpunit tests/CollectionTest.php`
106- Ok, I think that's enough refactoring for now.
107- Let's continue with our `Collection`, what's next `add()`, `remove()`, `contains`?
108- I think it's a toss up between `add()` and `contains`.
109- While there is work to do in `add()`, we can't really test it without `contains()`
110- These types of forks in the road will happen. Often, it doesn't really matter which you choose.
111- I'll start with `add()` as it will drive us to `contains()`.
112- We see we're really just using the other methods we have to help with the test.
113- `vendor/bin/phpunit tests/CollectionTest.php`
114- What's interesting is this test *auto-passes*. That is, it's already in a passing state even though we haven't written any code.
115- We can see that `add()` isn't *done*. It's not even tracking items in the *collection*.
116- This is what I meant about one driving the other.
117- So let's call `contains()` in our test for `add()`
118- Right now, `contains()` can just `return true`
119- `vendor/bin/phpunit tests/CollectionTest.php`
120- Again, everything is still passing.
121- That's okay though, because really, `add()` is just kind of hard to test.
122- It doesn't do anything immediately noticeable to the outside world.
123- `contains()` will help us test `add()` and `remove()`
124- So let's write our test for `contains()`
125- `vendor/bin/phpunit tests/CollectionTest.php`
126- Good, we finally have some failures.
127- In fact, anytime you've written a test that fails as you'd expect, it's a *good failure*.
128- Sometimes you can get in the habit of just seeing "red" and moving on.
129- Make sure the test is failing as expected.
130- Trust me, a bug in a test is worse than a bug in the code.
131- So, let's move on to our implementation of `contains()` as we all know `return true` isn't going to cut it.
132- Again, we'll just write the simplest thing that works - for me, that's a looping over the collection and returning true if the items match.
133- Just one last point about TDD - Writing the implementation is almost trivial when you write the test first.
134- `vendor/bin/phpunit tests/CollectionTest.php`
135- Great, we're passing.
136- So the last thing to implement in our interface is `remove()`
137- I'm going to give you all a minute to start this on your own.
138- Then I'll write my own and we'll compare...
139 - (write solution)
140- `vendor/bin/phpunit tests/CollectionTest.php`
141- Great, now that everything is written, let's take one last opportunity to refactor.
142- I want you guys to update the `Collection` class to only have the `$items` array property.
143- `vendor/bin/phpunit tests/CollectionTest.php`
144- Awesome, so this is our foundation for PHPUnit.
145 - (check time and show dataProvider for `contains` if possible)
146- Hopefully, you got a taste of TDD and the repetition got you familiar with running and writing tests.
147- (take a quick break, and we'll start on Mockery)
148
149# Mockery
150- So now that we've gone over some terms and reviewed *dependency injection*, let's look at *Mockery*.
151- The `Collection` object we made was easy to test because it didn't have any dependencies.
152- For this example, we're going to test a *repository* object.
153- This object will have a dependency of the database connection.
154- This time, we won't be test driving our code.
155- Instead, we'll test code that's already written.
156- This will give you a feel for writing tests for existing code and allow us to focus on *Mockery*.
157- So, feel free to download the `src` directory from the `mockery` branch of our test repo.
158- Since we are working with multiple classes, this code uses namespaces.
159- As such, we need to configure PHPUnit.
160- Create a `phpunit.xml` file in the root directory of the project.
161 - You are welcome to copy the one from the project on GitHub
162- There are two main configuration items of note.
163- First, the `bootstrap` attribute references a file PHPUnit will run before starting tests.
164- In this case, it loads Composer's *autoload* file. This way all of our classes are autoloaded.
165- Second, we specify the *tests* as the directory to automatically scan for our test case classes.
166- It will scan this directory for any file with a `Test.php` suffix.
167- `vendor/bin/phpunit`
168- When we run PHPUnit, we should see no test.
169- So, let's create our test.
170- Now that we adopted PSR-4 namespaces, I recommend having your `tests` directory mirror the structure of the `src` directory.
171- Create your `TaskRepositoryTest` within the `tests/Repositories` directory
172- Let's go ahead an fill in the `setUp` method to create our *subject*
173- At this point, we have the foundations of our test.
174- Since we're testing existing code, I like to bring them up side by side.
175- Unlike TDD, there's no need to find an entry point.
176- Let's just start at the top with `all()`.
177- To get our feet wet with Mockery, we're going to test the negative path first.
178- Basically we want to assert that we received an empty array because `$result` was `false`.
179- So, how can we do that...
180- What parts of the code can we hook into and manipulate?
181 - `$dbConnection`, right...
182- Specifically the `query` method.
183- So, let's use Mockery to create a Mock `mysqli` object and stub the query method to return `false`
184 - (write code)
185- Let's also be a little more strict with our assertion and use `assertSame`.
186- This will ensure it's not only the same value, but the same type.
187- `vendor/bin/phpunit`
188- Yay, it's passing.
189- However, sometimes when I *head shot* a test, I like to go back an manipulate it, just to ensure it failed.
190- So, let's change the `with()` arguments of our Mock.
191- `vendor/bin/phpunit`
192- And we see a failure.
193- It's worth noting that Mockery doesn't have the best error messages.
194- Fortunately there's on a few. So until you learn them, do your best to decrypt them to ensure you're failing for the right reason.
195- So, while we're on this negative path, let's be sure to test the other side of the `if` statement
196- We'll make another test to represent this different path.
197- It'll look roughly the same as our previous test.
198- However, this time instead of return `false`, we need to return a *result* who's `$num_rows` is 0.
199- If we check the docs, or in this case hover over the method, we see `query` return a `mysqli_result`
200- Let's make a mock and we'll have query return it.
201- Since `$num_rows` is just a property, we can simply set it directly on the mock.
202- `vendor/bin/phpunit`
203- Hmmm, this is an interesting error.
204- In fact, the first few times it threw me off too.
205 - (poll audience for ideas)
206- It turns out, `$num_rows` is effectively a *read-only* property.
207- Sometimes, you won't be able to mock a class directly.
208- In these cases, we can see just use a plain old object and set a property directly.
209- `vendor/bin/phpunit`
210- Great. I think that covers our negative paths
211- Let's do the *happy-path*.
212- Since we know `mysqli_result` is difficult to mock, we'll create our own Mock object.
213- We can no longer use a plain object since we need to control method calls.
214- Mockery allows you to create a *named* mock.
215- We'll stub the `fetch_assoc` method to return two values, then `false`.
216- Mockery returns the values in sequence for each time `fetch_assoc` is called.
217- If its called more than values, the last value is returned.
218- So, in order to properly stop our loop, we'll need to return a final value of `false`
219- There's one more thing I'd like to do.
220- I want to verify that we called `free`.
221- Although there is arguably a side-effect of the code and not normally something you would unit test, we'll pretend for the purposes of learning that you ran a memory intensive query and freeing the result is an important enough to validate with a test.
222- Mockery has its own set of expectations for Mocks.
223- In this case, we can use `shouldHaveReceived()` to verify `free` was called.
224- `vendor/bin/phpunit`
225- Oh no, another cryptic Mockery error.
226- In this case, we haven't stubbed the `free` method for our named mock.
227- We could do this, but it's a lot of code when we don't care about the arguments or return value.
228- Fortunately, Mockery has a `shouldIgnoreMissing` option to have any methods without expectations simply return `null`.
229- `vendor/bin/phpunit`
230- Yay! That did it.
231- So, we have `all()` pretty well tested.
232- Let's take a quick detour and prove that by running a code coverage report
233- `vendor/bin/phpunit --coverage-text --whitelist src`
234 - Don't worry if you can't run this, it requires Xdebug which may not be available on your machine.
235- We see that 2/3 methods in `TaskRepository` are covered.
236- That's the *constuctor* and `all()`
237- We'll look at this more later.
238- For now, let's get some more coverage.
239- So, I want you all to take a few minutes and write the *happy-path* test for `create`.
240- Okay, I'm going to write a quick implementation for comparison.
241- `vendor/bin/phpunit`
242- Great, how about the negative path for when `execute` fails
243- `vendor/bin/phpunit`
244- Awesome.
245- Let's run a quick code coverage report again.
246- This time, we'll do the HTML output to get a better idea of what's left.
247- Looks like we just need to test line 40.
248- Let's do so.
249- `vendor/bin/phpunit`
250- Oh noes, same *read-only* property issue.
251- However, this time, instead of creating a *named* mock, let's keep our *real* mock.
252- Instead, we'll do what J.B. Rainsberger calls pushing it to the boundary.
253- My team-mate calls this *squirrel it off*
254- Basically, anytime you have code that's difficult to test we can abstract that code out and move it somewhere else so it's easier to test.
255- In this case, let's assume we've extended `mysqli` and made a `getError` method as a getter for the read-only `error` property.
256- `vendor/bin/phpunit`
257- And voila. It works.
258- `vendor/bin/phpunit --coverage-text --whitelist src`
259- We're fully covered.
260- So this is pretty awesome.
261- Before break, let me close with a few final points.
262- First, we definitely wrote more test code using Mocks.
263- Sometimes this is unavoidable given your dependencies.
264- Just be sure you're not using mocks to test every line.
265- In fact, I don't advocate 100% test coverage. I've demonstrated it here simply to guide us on what's left to test.
266- Testing every line can make your code brittle as you don't have the opportuntity to refactor.
267- You only want to mock enough to run the desired test. The rest should still be black box.
268- Finally, all of our tests are passing even though `getError` is unimplemented in our real code.
269- I did this intentionally to emphasize that unit tests are only part the battle.
270- You can have all passing tests and 100% coverage and your code could still be broken.
271- So, let's take a short break then we'll look at writing integration tests with Codeception.
272
273# Codeception
274- So, Codeception has a little more setup required
275- In fact, it creates a lot of files.
276- When I'm using codeception, the first thing I want to do is separate my phpunit vs codeception tests.
277- Let's make these two folders in our `tests` directory
278- We'll also adjust our `phpunit.xml` configuration
279- `vendor/bin/phpunit`
280- Good everything still works
281- Now, I have internet connection so we'll just follow along their quick start.
282- Great. Now since I changed the default test folder, I'll need to adjust some of the paths
283- Codeception's configuration is a `codeception.yml` file
284- So let's adjust the paths
285- `vendor/bin/codecept run`
286- Similar to PHP, we haven't created any tests.
287- Let's do so.
288- Codeception has two types of test formats: *Cest* and *Cept*
289- I prefer the *Cest* as gives me test classes, making it easier to group my tests.
290- `vendor/bin/codecept generate:cest acceptance TaskList`
291- Note that we told Codeception this is an *acceptance* test.
292- If we look at the generate folders, Codeception makes three test *suites*.
293- While Codeception can run unit tests, I normally don't mix the two.
294- For now, let's remove the other two suites and we'll just use *acceptance*.
295- I keep *acceptance* instead of *functional* mainly because the default configuration for *acceptance* is better.
296- And since I primarily use Codeception to integration test my product, this more closely aligns with the *technical* definition of acceptance tests.
297- Ok. So before moving forward, let's look at what we're actually testing.
298- We'll fire up a simple PHP server.
299- `php -S localhost:8000 -t public`
300- This is just a little task list app to use the code we've written already.
301- Feel free to download the `public` folder from GitHub
302- Let's write our first integration test.
303- This will simply load up the page and ensure we don't have any tasks.
304 - (write test)
305- `vendor/bin/codecept run acceptance`
306- Oh noes, we didn't setup our test server.
307- So, let's configure the acceptance tests.
308- Inside `tests/codeception` is an `acceptance.suite.yml` file
309- Let's adjust the `url` to use the PHP server address
310- `vendor/bin/codecept run`
311 - Note, since we just have acceptance tests, we can save a few keystrokes and drop the suite name to just run everything.
312- Yay, it passes.
313- Let's write a test to add a task
314 - (write test)
315- `vendor/bin/codecept run`
316- Great, it works.
317- However, when we go to run our test again...
318- `vendor/bin/codecept run`
319- It fails.
320- What happened?
321- What we're seeing is known as *test pollution*
322- Our test for add is creating a record, so when our first runs, it doesn't have an empty state.
323- So, even with integration tests, we need to control the environment.
324- Codeception has many *modules*.
325- In this case, we'll use the `Db`
326- For now, we'll configure it to use our local database.
327- We also need to add the helpers to our acceptance suite and rebuild it.
328- `vendor/bin/codecept build`
329- We can include the necessary SQL scripts in our `_data` folder to rebuild our database
330 - **Note:** this should be a full dump of the test database. For demonstration purpose, I'm going to avoid this, but keep this in mind.
331- `vendor/bin/codecept run`
332- Now our tests are good again because we're clearing out the tasks table before every test
333- Let's update our add test now that we're using the Db helpers to ensure we see the correct record in the database.
334- `vendor/bin/codecept run`
335- Great.
336- For the final test, let's ensure that previously entered tasks appear correctly on our page.
337- We'll insert a few records.
338 - **Note:** while integration test data can be anything, be sure to add some variance. I've had passing tests due to some copy/paste bugs before.
339- Then we'll check that these values are on the page.
340- `vendor/bin/codecept run`
341- Yay!
342- Codeception has so much more than we've shown, so definitely browse the docs.
343 - (go to website)
344
345- I hope you've found this useful and can take one of these tools back with you to *start testing your PHP code*.
346- We have time for some questions.
347- Ok, we'll I'm here all week. So please come talk with me or sit with me at lunch. Glad to talk shop.
348- And, I'm @gonedark on Twitter. So follow me for updates about this talk and more...