I’m using Test Driven Development (TDD) for the first (and second) time on a couple of side projects. I wrote up some of my really early thoughts on the process before, but here are some further reflections on the process and issues I’ve come across.
Assurance is good!
It’s great having my test suite able to tell me that I’ve not broken any core functionality. The flexibility that this gives me to change the structure of my code (“re-factoring” they call it) without changing what it does is great, and the confidence I have when adding new functionality is wonderful.
A mindset change is (possibly) coming
I mentioned in the previous TDD article that I was thinking ahead a little as I coded and was wondering where to store those thoughts about what this thing will need to do in the future. And this is still a problem.
But I can see a slight change of mindset happening. I can see how I might be starting to think more in terms of features and functions as I write, and less in terms of data.
To explain, let’s take and example. I’m (stupidly) attempting to build my own analytics. In its very basic form this has “sites” and “views” and each view belongs to a site. But in future I can see that I would want multiple users and the ability to assign sites to users.
In the past I’d have got all hung up on making this ALL work together – define all my data structures, create my routes, and then build out all my models and controllers to make it all hang together. But with TDD I’ve taken Adam‘s approach from his course, and got right down to the core of what this application does – sites and views – and written my tests for those things and built them up in code.
So, right now, I have a working system with none of the complexity of users and site ownership.
I still have the issue of not knowing where to store my thoughts about future functionality. So for example, I might have a
sites_can_be_displayed test, but in future I will need:
So what then happens to my
sites_can_be_displayed test? Will that still work? Do I still need it? Do I need to review all my existing tests when I make a change? Or will this test break somehow when I add the new functionality in, prompting me to update or remove the it? Tricky.
I can also see that there might be overhead in building a feature at a time. If I anticipate features and build multiple features at the same time, I can get the benefit of knowing how it ALL hangs together from the start. Doing things a feature at a time might introduce inefficiencies that need to be re-factored away. Of course, having the tests helps with re-factoring, but it’s still overhead.
Redirects as responses
I realised quite early on that testing for HTTP responses in HTTP tests was good. You could test for exceptions as Jeffrey Way does on his Laracasts TDD course, but I’m not quite there yet. But checking for an appropriate response has been helpful.
BUT…you do need to understand what responses you should be getting. When building out user registration responses, for example, I forgot that Laravel redirects to somewhere immediately after registration. So my test call to:
$response = $this->post('/register', [ 'name' => 'Testy McTesterson', 'email' => 'firstname.lastname@example.org', 'password' => 'blahblah', 'password_confirmation' => 'blahblah', ] );
didn’t get a 200 response – my:
failed, it got a redirect instead! So I needed to…
Note that I also got caught out by the default password length check and forgot that this also redirects me back to the register page rather than giving me an HTTP error status.
(Aside: yes, I will use factories at some point)
And all this registration form testing leads to…
How much should I test?
One question I had as I went along is: How much should I test at this point?
Should I be testing the default password length check? The registration process at all? Can I just assume that this works?
I think I decided that I should test critical and/or customised parts – so I test user registration as that could contain custom fields or parameters that could change, but not login as that’s pretty much out of the box and doesn’t really modify any data in the system.
from() on form requests
The failed registration also raised another issue. The redirect it created uses the
back() method (I assume) to send you back to the page you filled the form in on. This, in turn, makes use of the referer header. Which I wasn’t sending.
So even when I’d written my
assertRedirect(), it was failing, because my
$this->post() request wasn’t sending a referer. So this relatively simple form-submitting test tripped me up in quite a few ways.
The fix for this was to use the
from() method to set the referrer – so the request in this test is actually:
$response = $this->from('/register')->post('/register', [ 'name' => 'Testy McTesterson', 'email' => 'email@example.com', 'password' => 'blah', 'password_confirmation' => 'blahblah', ] );
CSRFs and middleware
As I was writing the form submission test I thought: “But there’s a CSRF check. How do I pass that in my request?”
And, well, it turns out CSRF checks are disabled in the test suite which makes things a whole load easier.
But should I be bothered by this? Shouldn’t I be testing that
submitting_a_form_without_a_valid_csrf_token_fails? How do I know if I’m supposed to test this or not? Where do I go to find this out?
Laravel release notes
In attempting to find out if I should test CSRF checks I found (probably via some forum threads, I don’t remember) the
withoutMiddleware trait that you could use. This, it turns out, is optional up to Laravel 5.2, but it’s not – according to the docs – in the latest version of Laravel (5.6 at writing).
My instinct, when I found a thing that was documented in 5.2, but not 5.6 was to look for release notes for Laravel saying what has changed. But the latest release notes page doesn’t show any historical release notes.
This turns out to be stupidity on my part and you just need to use the version selector to view past release notes (e.g. these).
And in any case, the release notes turned out not to mention the
I guess the release notes can’t detail every tiny little thing that’s different, and not finding the release notes was kinda my own fault, but it was just another tiny little thing that piled on top of all the other things I was confused about and got in the way of my learning. But I was expecting the documentation to answer my question and it didn’t seem to exist and when I did find it it didn’t answer my question. Grrrr.
On getting to MVP slower and the value of TDD
On the face of it, getting to MVP is harder and slower. You write a test for a feature, and then build the feature. But in doing that you realise you need to spawn other tests for the same feature. E.g.
user_can_create_thing might lead to
Writing all these tests for the simple feature that you just built helps you refine the feature, sure. But it slows you down.
Or does it? Is this just because I’m learning? Will it speed me up later? Is this not really a good process for small side projects?
I think what I’ve come to realise is that there is an initial outlay/investment in getting your test suite off the ground. Those initial tests for simple stuff like creating a user are things that you will do on lots of projects. You’ll write factories for data early on and use them again and again in your future tests.
So for a relatively small side-project this overhead represents a significant part of the work and probably doesn’t get you much payback as you won’t run the tests as much.
But for a large project, the testing overhead is smaller and the value of the tests is much greater. So it will feel more worthwhile.
Where I’m at
I’m enjoying using TDD, but, as I just mentioned, for the small things I’m building with it, it feels like a lot of effort.
BUT…this is serving me well for the future. I HAVE to build these small projects using it because without them I won’t have confidence that I can use TDD on the larger projects that may come along.
I’m not a 100% convert to either the Wathan Way or the Jeffrey Way (the Way Way?) of TDD and I think they’d both say I should be pragmatic and thoughtful in my approach anyway rather than religiously following either of their approaches.
I’m being pulled up short a bit on unit testing too – this is something I need to delve into more. Currently most of my tests are feature/HTTP tests. And unit tests feel a bit like:
$test = x; $this->assertEquals(x, $test);
I mean, not quite like that – it’s an extreme example. But you get the idea. And I’m sure I feel this way because I’m testing the wrong things. So still plenty to learn.
Leave your TTD tips and tricks and wisdom for me and others below. Thanks!