I chose PHPUnit and created a directory for our tests already. To get started two things left:
create tests/phpunit.xml which PHPUnit loads automatically
create tests/bootstrap.php the entry point for the tests suite
phpunit.xml so far is this:
Tests suite’s entry point as any entry point of our app must define APPLICATION\PATH constant, include path and an autoloader so models can be found and loaded.
for the autoloader I used the one that comes with Zend Framework.
First thing to do is to test whether we’ve brought all our domain model requirements to the code correctly. You do not need a database for that. The tests must cover all constraints you have in your domain model and nothing else should be taken into account.
Let’s arrange “Tests” to be the root namespace for our tests. Tests for the domain model will be located in the tests/Domain directory and have the namespace Tests/Domain accordingly.
For instance, here are the requirements for the Subscription, the simplest domain entity:
has a unique identifier for reference
has a name
As the implementation reflects the domain model itself, the tests reflect these constraints. Let’s take a look at tests/Domain/SubscriptionTest.php
It’s not needed to choose what should be covered by unit tests. You test everything that your domain model dictates. To not spoil it, never change tests without changing the domain model actual requirements. And vice versa.
Now, let’s test something more interesting, the User entity.
has a unique identifier for reference
has a name
has a valid email
has a password not shorter than 6 characters, hashed
may be either admin or not admin
is not admin by default
there’s a way to define whether a user is an admin or not
belongs to a single company
Here is the test case tests/Domain/UserTest.php, check out the comments:
Service Layer serves not only for application logic purposes but conveniently groups the upper layer of domain logic. One service layer function = one atomic business operation which we must test.
While repositories are also domain model objects we only use standard Doctrine 2 features in our scenario. Something already covered with tests by Doctrine 2 developers. The same goes for the persister i.e. EntityManager in our case: it’s been already very well tested, so we only need to make sure everything we expect to be called is called and all requirements are reflected in the code.
Same as for Company in the User test above we need neither real repositories nor real entity manager. We’ll use PHPUnit built-in mock builder to fake those and utilize the ServiceLocator capabilities to replace the implementation of repositories and the manager so Service Layer gets mocks instead.
For that we’ll need to add two methods to models/ServiceLocator.php:
The requirements for the Service Layer’s registerCompany scenario look like this:
finds the subscription plan by its identifier in the data storage
error if the plan is for some reason not found
error if two passwords provided are not equal
creates new user admin account based on the email, name and password provided
creates company based on company name provided, new admin user and the subscription plan found
saves the new company in the data storage
This concludes testing the Domain Model. Whether you have something like user stories or not you’re free to change the order of things: you can write tests first, you can implement the logic keeping the domain requirements in your head and then test the logic.
You just need to maintain integrity. If you change one part - the changes should be done in all others. For instance you wrote the tests and are starting the implementation. You occasionally see that you can’t code what you really needed in terms of the Domain Model requirements you have. Stop at that moment and extend the user stories with everything you need, then update the tests so they follow the new requirements and then complete the implementation.
I don’t personally like UMLs and have no problems with mixing orders of things getting into place according to the original task.
Software isn’t just about the domain model. We have things such as sending emails, database transactions. Things that need to be tested as well. What sense make our classes if our Service can’t actually save anything to the database?
We need more tests and these are not “Domain”. I call them “Functional” which is also the namespace they belong to and the directory, i.e. tests/Functional. They’re called so because of the new point of view. We don’t care anymore what’s inside of our ServiceLayer functions, we don’t need to know their structure, implementation details and patterns applied. We just need to make sure that those “functions” do what they are supposed to do in the end.
In this specific case the only thing we have to do is to test whether the CompanyService::registerCompany() actually adds our new company and user data to the database tables. The test case is “tests/Functional/CompanyServiceTest.php” which groups tests for all functions related to companies operations.
PHPUnit has everything we need for testing databases.
Before any test executed it truncates the tables (*), fills them with data from XML-formatted fixtures. Every database test should check that after a certain operation that affects the DB the data in it matches our expectations about it.
We’ll have a file tests/Functional/files/company-service.xml, a fixture for CompanyServiceTest. We need to put there a couple of subscription plans so they can be found by CompanyService::registerCompany method:
The testRegisterCompany will call the method and make sure that the new data appeared in the tables. New data in this case is an admin user and a company. To do that we use another data set, tests/Functional/files/register-company.xml:
This is enough, except that the main database can’t be used for tests. We need a copy. I called mine basiccrm\tests. To make tests environment use this new database instead,I introduced sections to the config.ini configuration file, like this:
The “tests” section inherits from “main” (which is for the main app) and in order to use it I added the following line to the test suite entry point: