-
Notifications
You must be signed in to change notification settings - Fork 3
Unit Testing
It is highly recommended that all packages make extensive use of tests to prevent bugs.
You can add a tests
directory to any directory in the app
folder of the package. You can run all the package's tests using phpunit
or run a specific test/directory of tests using phpunit app/Report
for example. scp-package-seed
comes with a phpunit.xml
already in place that you are welcome to modify as necessary.
All tests should extend App\Support\Test\TestCase
to make use of the methods there, e.g. auth
, authNewClient
, etc.
Numerous authentication methods are available:
/**
* @param Authenticatable $user
*
* @return Authenticatable
*/
public function auth(Authenticatable $user);
/**
* @return $this
*/
public function logout();
/**
* Authorize as a new Administrator.
*
* @param string $username
* @param string $password
*
* @return Admin
*/
public function authNewAdmin($username = '_testing_', $password = '_testing_');
/**
* Authorize as a client.
*
* @return Client
*/
public function authNewClient();
Factories are useful for generating Model
s. You can use factories like so:
public function setUp()
{
parent::setUp();
$this->server = factory(\App\Server\Server::class)->create([
// This attribute map will be applied to the Server automatically,
// and the server will be created and saved for you.
'billing_date' => '<some value>',
]);
}
You should be sure to delete all the testing items that were created at the end of each test:
public function tearDown()
{
parent::tearDown();
$this->delete(
$this->server->url()
);
}
Using the API to delete ($this->delete(...)
) the items you create (in the right order) will automatically delete any relationships that are preventing the Model
from being deleted.
Mocks are classes that pretend to be another class. So if you had a Mock Carbon
instance and called the gt
method on it, the Mock would throw an error that the gt
method is not mocked. If you ran ->shouldReceive('gt')
, the Mock would allow the gt
function to be run. In this way, we can specify the exact methods we expect to be called on any mocked object when it is used by a Service we are testing. Also, we can check that the arguments sent to these methods are correct and we can specify return values for the mocked methods.
Unit tests should only verify that one single class works as expected. If a class has any injected dependencies on other classes, those classes should be mocked. If the class fires any events, for instance, it should be done through a mocked EventDispatcher
.
So the basic idea behind this form of unit testing is:
- Prove that a single service class works as expected - i.e. when you call a certain method it performs the correct logic.
- Prove that every class using that class works as expected - i.e. when you POST to some controller, it calls a certain method on your service. Since you've Mocked your service class, the method is not actually called but you can verify that it is called in the controller with the right arguments using the Mock.
Here's an example Unit Test using mocks:
class Service
{
public function __construct(OtherService $other)
{
$this->other = $other;
}
public function doSomething(Carbon $date)
{
if ($date->gt($this->other->someDate()) {
$this->other->dateIsGreater($date);
return;
}
$this->other->dateIsLess($date);
}
}
class ServiceTest extends TestCase
{
public function setUp()
{
parent::setUp();
$this->service = new Service(
$this->other = Mockery::mock(OtherService::class)
);
}
public function testDateGreater()
{
$date = Mockery::mock(Carbon::class);
$otherDate = Mockery::mock(Carbon::class);
$this->service->doSomething($date); // This errors, don't do this yet! `gt` is not mocked.
$this->other->shouldReceive('someDate')->andReturn($otherDate);
$date
->shouldReceive('gt')
->with($otherDate)
->andReturn(true)
;
$this->other->shouldReceive('dateIsGreater')->with($date);
}
public function testDateGreater()
{
$date = Mockery::mock(Carbon::class);
$otherDate = Mockery::mock(Carbon::class);
$this->other->shouldReceive('someDate')->andReturn($otherDate);
$date
->shouldReceive('gt')
->with($otherDate)
->andReturn(false)
;
$this->other->shouldReceive('dateIsLess')->with($date);
}
}
In this way, we are able to verify that the logic in the Service works exactly as expected.
In the example above, if OtherService::dateIsLess
normally modifies $date
, we will no longer have that modification happening in the unit test. This is actually good behavior. We want a single unit test to only test a single class' logic. If we need the modification from dateIsLess
to happen to $date
we need to modify it ourselves in the unit test. OtherService
should be tested by a separate TestCase that verifies that it works as expected. If OtherService
and Service
are both tested properly, we will detect any change in the logic of either because the unit tests will fail. Then, we can adjust the unit tests to work with the new logic.