Often programmers confuse controller testing with unit testing. They see that a functional test offers more code coverage and so programers can be tempted to focus on controller testing rather than testing units of code.
A registration process chain
Lets think about the life cycle of a basic a controller that handles user registrations.
A user has just registered to website by submitting a form. That request gets sent to a controller which deals with the requested data and then issues a response back to the user. In doing so these tasks are performed:
- A request object is constructed and passed to the controller
- A form is built with a series of validation constraints
- Once the form is submitted the sent data is sanitised. After it has been cleaned the request object is synchronised with the form which allows the validation process to begin
- The database is queried to ensure that the user details (Perhaps email address) is unique
- In the event of a validation error a translated message is generated with the correct format
- In the event of a successful submission a user will be created and saved to the databases
- Perhaps an email is sent with an activation code. If so this unique and expiring code would need to be generated and saved to the database
- Perhaps the user is automatically signed into the website after successful registration. In this case the session will be manipulated
- A template is retrieved and its view is constructed by injecting values (possibly translated) into placeholders. This becomes the response
- Finally the controller returns the response
Now the above is perhaps an oversimplified list of controller operations. However it gives an indication to the scale of processes that controllers have to deal with. These processes are chained together in control flows before a response is returned.
In the above example of the user registration the following dependencies are needed:
- Request object
- Form object
- Form handler
- Validation routines
- Database connection
- Database queries
- User model management
- Translation mechanism
- Session access
- Template engine
Many of these dependecies can be classed as services that the controller calls upon to perform certain tasks.
Its fair to say that the controller is nothing more than bucket of tasks that are glued together. A request comes in, the request is handled and a response is returned. The controller creates a chain of events or processes that occur based on the output of the last process.
- An activation email wont be sent if the user hasn’t been saved to the database.
- The user wont be saved to the database if the validation rules return any errors.
- The validation returns errors if the user has submitted incorrect data and so on and so forth.
Its not as black and white (but very similar to) the unix style stdIn and stdOut approach. These input streams can be redirected to small and individual programs that form a command chain. In this context the individual programs would be the services that the controller depends on. The stream redirection would indicate the control structure which channels the controllers process flow. Obviously we shouldn’t think of a controller as a mere set of input redirections but this line of thinking helps us (Me at least) to isolate the processes that controller performs into services and then eventually into units of code.
Focus on the service operations and not the process chain
In the registration example there are lots of services compared to a single registration process. These services will be used within other process chains. The emailer for example will not be limited to just the registration process. Perhaps there is a newsletter that utilises this service.
When testing a controller the whole process chain gets executed and tested. This might seem like a good thing to do and it will certainly increase code coverage but the services are only being tested in the context of the controller that is being executed. Even if the registration tests have passed and an activation email has been sent we cannot assume that a newsletter mailshot will also work.
When testing individual service operations we are ensuring that the foundations are stable for any controller that requires that service. We are also removing the context in which the test would normally be performed (The controller). Testing the send method of the emailer service outside of the context of the registration controller ensures that any controller using that service will have a stable send operation.
Controller testing should not be forgotten
Even if all the services and their operations have passing unit tests the system will still be at risk of malfunction. If the unit tests are passing then the bug would be easier to spot. It will probably be within the process chain. – In the controller itself.
Controller testing should not be classed as unit testing and should not be treated as such.
Controller testing is functional testing and perhaps should be done after all the unit tests have been performed.