Testing the User’s Experience, not your code
So, you’re trying Test Driven Development. Your speed might have slowed down, and you’re not sure that you’re gaining that much confidence from having the tests. But also, talking to colleagues about it is scary, because those who are the most experienced with TDD always seem so sure.
Don’t worry, we often have to rethink our testing practices too. But, we often come from a few first-principles. They turn the concept of TDD into a methodology, a specific approach that helps you know where to go next when creating a new feature or fixing a bug. The top three I hear about most, are:
- Outside-In Testing
- Black Box Test Design
- Red, Green, Refactor
Today, we’ll go with the first.
Outside-In Testing
There are a lot of different tests you can write, at varying levels of complexity and time commitment. Writing quality integration or system tests takes a lot of time and effort, where writing nothing but unit tests feels fragile, and like you might be missing something.
What can help, is thinking about your task from the point of view of the user, and follow that logic deeper and deeper. In other words, outside-in testing.
Generally, this means starting with some kind of Integration/System test(the difference is primarily semantic in the Rails community- they do almost the same thing, but Integration tests don’t run Javascript, where System tests do). Lets start with an example story:
As a user, I would like to have a dashboard of employees at my company, and if I’m an administrator, to be able to change each user’s type from normal to administrator using a toggle.
So, you’re already putting together the pieces. There’s a lot here to implement before I can make an integration test- I’ll need at minimum a controller with both an index and update method, a view, let alone a way of determining if a user can see the toggle buttons.
That’s what pending tests are for.
When run, these tests will fail, with the message that these tests are still pending. Great! Now we can go deeper.
In this case, because it’s a traditional CRUD route, it actually isn’t super useful to use controller tests. This is part of why the Rails community moved away from them- system tests can accomplish the exact same thing, and you’re actually bothering to guarantee what’s rendered.
So, you start building. First you add the new route and controller, then the first controller method- #index:
def index
@users = User.all
endSo you move on to creating the template. And most of the .erb is simple and self-explanatory- until you reach whether to render the Admin toggle.
Now in an example this simple, you could likely just call .admin? and call it a day. But often in large applications, something like an authorization policy system is necessary. In this case, we might have a UserPolicy that can ask if the user can access the update controller method, or if they should get a 403 forbidden. This means, adding a new method to UserPolicy.
So, you write the tests for it:
Then, implement the policy method:
And tahdah. The test goes green. So now, you finish writing the template, and update your integration tests:
So of course, you can and should go further than here. What happens when a user is toggled? What happens if a normal user tries to promote another user? Anything that could be taken as business logic, or might significantly change a user’s experience, should be written into an integration test. Niche cases are fair for unit tests.
Just keep in mind: Test code is real code, and like real code, it can both help, or be a liability. Everything in your application should be tested, within reason. If behavior doesn’t significantly change based on additional code, or if it’s code that’s internal to a dependency, you might skip testing it. Just be sure to test that it does what it says!
Next time, we’ll talk about Black Box testing. See you Monday!
