- Basic usages with a simple demo app
- Databsae preparation for the tests
- Writing a ControllerTest
- Writing tests
To demonstrate to you how to use the bundle, we have created a sample project. You can find it here and reproduce it to try our bundle.
Lets look to the Demo Entity first. To stay basic, it's only defined by 2 properties, a name in string and a value in int.
Now that we can store data, what if we wanted to use it ? Let's create a DemoController. As TestApiRestBundle only handle json response, the controller won't return anything but serialized object. Here's an example with a GET route, displaying all the data stored in the Demo Entity.
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$encoders = array(new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$serializer = new Serializer($normalizers, $encoders);
$demos = $em->getRepository('AppBundle:Demo')->findAll();
$response = $serializer->serialize($demos, 'json');
return new Response($response, 200, ['Content-Type'=>"application/json"]);
}
Do the same for a POST and a DELETE route, and our simple app become pretty complete. But before writing our first test we still have a few preparation to do.
As each tests are independant, the database must be reload at the beginning of them to an equivalent state. The classic method is to destroy the database, re-create it with its scheme and its fixtures data. Bigger the database is, longer the time the tests take to process. So, for performance and productivity reason, TestApiRestBundle acts differently : it reloads the database state at each setup using files.
All you need to have is a database_url
set in your doctrine configuration.
⚠ Only mysql and sqlite mode are supported by TestApiRestBundle.
Before launching the tests, you must prepare your database. To do so, you can use the bundle doctrine/doctrine-fixtures-bundle
to help you generate a test database. Then, dump it in the var/data/db_test/
diretory of your project.
⚠ Be aware that in mysql mode the tests are more rigorous as it takes into account the relations constraints. But they are much slower to process than in sqlite mode due to the resetting method.
Each of your tests are located in the tests
folder of your application as it should be. For each of your controller you want tested, create a class in tests\Controller
with the name of it and the suffix Test
. In our exemple, let's say DemoControllerTest
. And make it extend JsonApiAsArrayTestCase to use our powerful tools.
TestApiRestBundle uses the concept of dataProvider to load the tests. But for readability reason, you won't hard write the array containing all your tests in your ControllerTest. So TestApiRestBundle would rather load an organized yaml file and parse it :
const YAML_PROVIDER_FILENAME = "demo";
public static function ApiCallProvider()
{
return ResourcesFileLoader::testCaseProvider(__DIR__,self::YAML_PROVIDER_FILENAME);
}
?>
We said earlier that TestApiRestBundle creates a database file for each fixture you have. To use it while the reset of your database, add the setUp method :
const FIXTURE_FILENAME = "LoadDemoFixtures";
public function setUp($fixtureFilename = null)
{
parent::setUp(self::FIXTURE_FILENAME);
}
?>
Finally, to execute the tests and make assertions, you juste need to add the testAPICall function and pass it the dataProvider annotation to use ApiCallProvider.
/**
* @dataProvider ApiCallProvider
*/
public function testAPICall($data_test)
{
$this->genericTestAPICall($data_test);
}
?>
And here's the completed class :
<?php
namespace EveryCheck\TestApiRestBundle\Doc\Example\sampleProject\tests\Controller;
use EveryCheck\TestApiRestBundle\Controller\JsonApiAsArrayTestCase;
use EveryCheck\TestApiRestBundle\Loader\ResourcesFileLoader;
class DemoControllerTest extends JsonApiAsArrayTestCase
{
const YAML_PROVIDER_FILENAME = "demo";
const FIXTURE_FILENAME = "LoadDemoFixtures";
/**
* @dataProvider ApiCallProvider
*/
public function testAPICall($data_test)
{
$this->genericTestAPICall($data_test);
}
public static function ApiCallProvider()
{
return ResourcesFileLoader::testCaseProvider(__DIR__,self::YAML_PROVIDER_FILENAME);
}
public function setUp($fixtureFilename = null)
{
parent::setUp(self::FIXTURE_FILENAME);
}
}
You can use this template in your own application and adapt the constants values and class name to your files.
With TestApiRestBundle, you can test with 2 different ways. You can either test an unique kind of request to an endpoint to assert the comportment of that endpoint specifically. Since it is unique, we call it unit_test and TestApiRestBundle reset the database test after each one on them. Either you can test a bunch of request, sent in a particular order and without database resetting between them, to test globally the comportment of your system. We call that pack of test a scenario.
The tests aren't actually written in the ControllerTest class but in a yaml file, in the Resources\config
folder. To write your first unit test, start with the following tag:
unit_tests:
Each time you want to write an unit test, do it under the unit_tests
tag.
Unit tests are separated by route methods.
⚠ For now, GET, POST, PATCH, PUT and DELETE methods are suppported for unit testing
So, let's say you want to test a DELETE route. To do so, add a DELETE tag :
unit_tests:
DELETE:
A unit test is then defined by an array with keys to . Here's the list of all the possible key :
Key | Purpose | Default value |
---|---|---|
url | to which endpoint the request is sent | |
out | the expected response | |
in | the data sent with the request | |
status | html code of the response | 200 |
headers | custom headers sent with the request | |
ct_in | specify the content-type of the request | application/json |
ct_out | specify the content-type of the response | application/json |
the expected number of email sent at the end of the request | ||
pcre_mail | assert the presence of a value in an sent email via RegExp |
If you want to know more about the ct_in
available, you can read this.
So, if you want to test that your app send a 404 html code when you try to delete a non-found resource, you'd write the following test :
- { url: "/demo/a" , status: 404 }
Now, add a POST tag and a new test under it :
unit_tests:
POST:
- { url: "/demo/new", status: 201, in: "postValidDemo" , out: "postedDemo" }
DELETE:
- { url: "/demo/a" , status: 404 }
As you can see, the in
and out
keys doesn't contain actual data, but the name of a json file. That's where you write the body of a request or of a response, for readability matters. So let's create them.
The in
refers to the actual body content of the request you would send to your endpoint. The out
corresponds to the response you should get from that request. By defaut, those files are stored in the tests\Payloads
and the tests\Responses\Expected
folder. By editing your conf.yaml
you can change that default folder. You can also modify it for a particular test :
unit_tests:
POST:
- { url: "/demo/new", status: 201, in: "../path/to/payloads/postValidDemo" , out: "../../path/to/expected/responses/postedDemo" }
When this test is processed, TestApiRestBundle checks if those json data match or not.
You can write as many unit tests as you want but keep in mind the structure of the file :
unit_tests:
GET:
- { url: "/demo" , status: 200, out: "listOfDemos"}
PATCH:
- { url: "/demo/1" , status: 405}
PUT:
- { url: "/demo/1" , status: 405}
POST:
- { url: "/demo/new", status: 400, in: "postInvalidDemo" }
- { url: "/demo/new", status: 201, in: "postValidDemo" , out: "postedDemo" }
DELETE:
- { url: "/demo/a" , status: 404 }
- { url: "/demo/1" , status: 204 }
In the case you need to send cookies in your test request, use the header
keyword with the following pattern :
GET:
- { url: "/demo/cookies" , status: 200, out: "sentCookies", headers: "Cookie : cookiename=cookievalue"}
Scenario tests are supposed to ensure the global comportment of your app. They are nothing more than a pack of unit tests without database reset between them. Before writing a scenario, add the tag at the end of your unit tests :
unit_tests:
GET:
- { url: "/demo" , status: 200, out: "listOfDemos"}
PATCH:
- { url: "/demo/1" , status: 405}
PUT:
- { url: "/demo/1" , status: 405}
POST:
- { url: "/demo/new", status: 400, in: "postInvalidDemo" }
- { url: "/demo/new", status: 201, in: "postValidDemo" , out: "postedDemo" }
DELETE:
- { url: "/demo/a" , status: 404 }
- { url: "/demo/1" , status: 204 }
scenario:
A scenario is then defined by its name and the same kind of arrays that you used for unit tests :
scenario:
creation_then_deletion:
- { action: "DELETE", url: "/demo/3", status: 404}
- { action: "POST" , url: "/demo/new" , status: 201, in: "postValidDemo" , out: "postedDemo"}
- { action: "DELETE", url: "/demo/3", status: 204 }
You can use all the keys of the unit tests, plus another one : the action
.
Key | Purpose | Default value |
---|---|---|
action | which method must be performed for the request | |
url | to which endpoint the request is sent | |
out | the expected response | |
in | the data sent with the request | |
status | html code of the response | 200 |
headers | custom headers sent with the request | |
ct_in | specify the content-type of the request | application/json |
ct_out | specify the content-type of the response | application/json |
the expected number of email sent at the end of the request | ||
pcre_mail | assert the presence of a value in an sent email via RegExp |
Its purpose is the specify which method must be used by the request. And speaking of methods, scenario testing also support GET, POST, PATCH, PUT and DELETE and a new one called DB. This one is used to ensure that our app acts well even in the database :
scenario:
db_testing:
- { action: "POST" , url: "/demo/new" , status: 201, in: "postValidDemo" , out: "postedDemo"}
- { action: "DB", url: "TestApiRestBundle:Demo?id=3", out: "dbTesting" }
The url here is quite different. It is defined by the entity you want to check, then a ?
and the property to check. In the example above, the scenario is checking that after a POST, there is in the database a Demo with 3 as its id.