Important
You are browsing the documentation for version 3.1 of OroCommerce, OroCRM and OroPlatform, which is no longer maintained. Read version 5.1 (the latest LTS version) of the Oro documentation to get up-to-date information.
See our Release Process documentation for more information on the currently supported and upcoming releases.
Functional Tests¶
Functional tests check the integration of the different layers of an application.
In this article you will learn how you can improve the experience of writing functional tests with OroPlatform. It is recommended to read the Symfony documentation concerning testing before you continue. You should also be familiar with PHPUnit.
When to Write Functional Tests¶
Functional tests are generally written for:
Controllers
Commands
Repositories
Other services
The goal of functional tests is not to test separate classes (unit tests), but to test the integration of the different parts of an application.
Add functional tests to supplement unit tests for the following reasons:
You can test the multi-component system and ensure it works as a whole.
You can skip mocking the complicated interface or data manipulation layer (like doctrine classes to build a query).
Unit tests can pass even when functionality works incorrectly.
Test Environment¶
Initialization Client and Loading Fixtures Caveats¶
To improve the performance of test execution, the initialization of a client is only done once per test case by default. This means that the kernel of the Symfony application will be booted once per test case. Fixtures are also loaded only once per test case by default. On the one hand, initializing and loading fixtures once per test case increases the performance of test execution but it can also cause bugs because the state of fixtures and the kernel (and as a result, the service container) will be shared by default between test methods of separate test cases. Be sure to reset this state if necessary.
Test Environment Setup¶
You need to configure the following parameters for the testing environment:
Create a separate database for tests (e.g., add ‘_test’ suffix):
Set up host, port and authentication parameters for the database, the mail server, and the web socket server in the parameters_test.yml file:
For example:
1# config/parameters_test.yml 2parameters: 3 database_host: 127.0.0.1 4 database_port: null 5 database_name: crm_test 6 database_user: root 7 database_password: null 8 mailer_transport: smtp 9 mailer_host: 127.0.0.1 10 mailer_port: null 11 mailer_encryption: null 12 mailer_user: null 13 mailer_password: null 14 session_handler: null 15 locale: en 16 secret: ThisTokenIsNotSoSecretChangeIt 17 installed: '2014-08-12T09:05:04-07:00'
Install the application in the test environment:
$ bin/console oro:install --env=test --organization-name=Oro --user-name=admin --user-email=admin@example.com --user-firstname=John --user-lastname=Doe --user-password=admin --sample-data=n --application-url=http://localhost
During installation, the database structure is set up and standard fixtures are loaded.
Run tests using phpunit with the proper –testsuite option (unit or functional).
Caution
Currently, running different types of automated tests together is not supported. It is, therefore, strongly not recommended to run unit tests and functional tests side by side in one run as this produces errors. Unit tests create mock objects that later interfere with functional test execution and create unnecessary ambiguity. It is possible to disable unit tests on test startup with the help of the test suite option:
$ php bin/phpunit -c ./ --testsuite=functional
$ php bin/phpunit -c ./ --testsuite=unit
Database Isolation¶
The @dbIsolationPerTest
annotation adds a transaction that will be performed
before a test starts and is rolled back when a test ends.
1// src/Oro/Bundle/FooBundle/Tests/Functional/FooBarTest.php
2namespace Oro\Bundle\FooBundle\Tests\Functional;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6/**
7 * @dbIsolationPerTest
8 */
9class FooBarTest extends WebTestCase
10{
11 // ...
12}
Loading Data Fixtures¶
Use the Oro\Bundle\TestFrameworkBundle\Test\WebTestCase::loadFixtures
method to load a fixture in a test:
1// src/Oro/Bundle/FooBundle/Tests/Functional/FooBarTest.php
2namespace Oro\Bundle\FooBundle\Tests\Functional;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6class FooBarTest extends WebTestCase
7{
8 protected function setUp()
9 {
10 $this->initClient(); // must be called before!
11
12 // loading fixtures will be executed once, use the second parameter
13 // $force = true to force the loading
14 $this->loadFixtures([
15 'Oro\Bundle\FooBarBundle\Tests\Functional\DataFixtures\LoadFooData',
16 '@OroFooBarBundle/Tests/Functional/DataFixtures/bar_data.yml',
17 ]);
18 }
19
20 // ...
21}
A fixture must be either a class name that implements Doctrine\Common\DataFixtures\FixtureInterface
or a path to the nelmio/alice file.
An example of a fixture:
1// src/Oro/Bundle/FooBarBundle/Tests/Functional/DataFixtures/LoadFooData.php
2namespace Oro\Bundle\FooBarBundle\Tests\Functional\DataFixtures;
3
4use Doctrine\Common\DataFixtures\AbstractFixture;
5use Doctrine\Common\Persistence\ObjectManager;
6use Oro\Bundle\FooBarBundle\Entity\FooEntity;
7
8class LoadFooData extends AbstractFixture
9{
10 public function load(ObjectManager $manager)
11 {
12 $entity = new FooEntity();
13 $manager->persist($entity);
14 $manager->flush();
15 }
16}
1 # src/Oro/Bundle/FooBarBundle/Tests/Functional/DataFixtures/bar_data.yml
2 Oro\Bundle\FooBarBundle\Entity\BarEntity:
3 bar:
4 name: test
You can also implement the Doctrine\Common\DataFixtures\DependentFixtureInterface
which enables to load fixtures depending on other fixtures being already loaded:
1// src/Oro/Bundle/FooBarBundle/Tests/Functional/DataFixtures/LoadFooData.php
2namespace Oro\Bundle\FooBarBundle\Tests\Functional\DataFixtures;
3
4use Doctrine\Common\DataFixtures\DependentFixtureInterface;
5use Doctrine\Common\DataFixtures\AbstractFixture;
6use Doctrine\Common\Persistence\ObjectManager;
7
8class LoadFooData extends AbstractFixture implements DependentFixtureInterface
9{
10 public function load(ObjectManager $manager)
11 {
12 // load fixtures
13 }
14
15 public function getDependencies()
16 {
17 return ['Oro\Bundle\FooBarBundle\Tests\Functional\DataFixtures\LoadBarData'];
18 }
19}
Further, you can use reference-specific entities from fixtures, e.g.:
1namespace Oro\Bundle\FooBarBundle\Tests\Functional\DataFixtures;
2
3use Doctrine\Common\Persistence\ObjectManager;
4use Doctrine\Common\DataFixtures\DependentFixtureInterface;
5use Doctrine\Common\DataFixtures\AbstractFixture;
6
7use Oro\Bundle\FooBarBundle\Entity\FooEntity;
8
9class LoadFooData extends AbstractFixture implements DependentFixtureInterface
10{
11 public function load(ObjectManager $manager)
12 {
13 $entity = new FooEntity();
14 $manager->persist($entity);
15 $manager->flush();
16
17 $this->addReference('my_entity', $entity);
18 }
19
20 public function getDependencies()
21 {
22 return ['Oro\Bundle\FooBarBundle\Tests\Functional\DataFixtures\LoadBarData'];
23 }
24}
Now, you can reference the fixture by the configured name in your test:
1// src/Oro/Bundle/FooBundle/Tests/Functional/FooBarTest.php
2namespace Oro\Bundle\FooBundle\Tests\Functional;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6class FooBarTest extends WebTestCase
7{
8 protected $entity;
9
10 protected function setUp()
11 {
12 $this->initClient();
13 $this->loadFixtures('Oro\Bundle\FooBarBundle\Tests\Functional\DataFixtures\LoadFooData');
14 $this->entity = $this->getReference('my_entity');
15 }
16
17 // ...
18}
Hint
By default the entity manager is cleared after loading each fixture. To prevent clearing a fixture
can implement Oro\Bundle\TestFrameworkBundle\Test\DataFixtures\InitialFixtureInterface
.
Hint
Sometimes you need a reference to admin organization, user or business unit. The following fixtures can be used to load them:
Oro\Bundle\TestFrameworkBundle\Tests\Functional\DataFixtures\LoadOrganization
Oro\Bundle\TestFrameworkBundle\Tests\Functional\DataFixtures\LoadUser
Oro\Bundle\TestFrameworkBundle\Tests\Functional\DataFixtures\LoadBusinessUnit
Writing Functional Tests¶
To create a functional test case:
Extend the
Oro\Bundle\TestFrameworkBundle\Test\WebTestCase
classPrepare the test client (an instance of the
Oro\Bundle\TestFrameworkBundle\Test\Client
class)Prepare fixtures (optional)
Prepare container (optional)
Call test functionality
Verify the result
Functional Tests for Controllers¶
The Control Flow¶
A functional test for a controller consists of a couple of steps:
Make a request
Test the response
Click on a link or submit a form
Test the response
Rinse and repeat
Prepare Client Examples¶
Simple initialization works for testing commands and services when authentication is not required.
1// src/Oro/Bundle/FooBundle/Tests/Functional/FooBarTest.php
2namespace Oro\Bundle\FooBundle\Tests\Functional;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6class FooBarTest extends WebTestCase
7{
8 protected function setUp()
9 {
10 $this->initClient(); // initialization occurres only once per test class
11 // now varialbe $this->client is available
12 }
13 // ...
14}
Initialization with custom AppKernel options:
1// src/Oro/Bundle/FooBundle/Tests/Functional/FooBarTest.php
2namespace Oro\Bundle\FooBundle\Tests\Functional;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6class FooBarTest extends WebTestCase
7{
8 protected function setUp()
9 {
10 // first array is Kernel options
11 $this->initClient(['debug' => false]);
12 }
13 // ...
14}
Initialization with authentication:
1// src/Oro/Bundle/FooBundle/Tests/Functional/FooBarTest.php
2namespace Oro\Bundle\FooBundle\Tests\Functional;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6class FooBarTest extends WebTestCase
7{
8 protected function setUp()
9 {
10 // second array is service options
11 // this example will create client with server options ['PHP_AUTH_USER' => 'admin@example.com', 'PHP_AUTH_PW' => 'admin']
12 // make sure you loaded fixture with test user
13 // bin/console doctrine:fixture:load --no-debug --append --no-interaction --env=test --fixtures src/Oro/src/Oro/Bundle/TestFrameworkBundle/Fixtures
14 $this->initClient([], $this->generateBasicAuthHeader());
15
16 // init client with custom username and password
17 $this->initClient([], $this->generateBasicAuthHeader('custom_username', 'custom_password'));
18 }
19 // ...
20}
Types of Functional Tests¶
Testing Controllers¶
Have a look at an example of a controller test from OroCRM:
1// src/OroCRM/Bundle/TaskBundle/Tests/Functional/Controller/TaskControllersTest.php
2namespace Oro\Bundle\TaskBundle\Tests\Functional\Controller;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6/**
7 * @outputBuffering enabled
8 */
9class TaskControllersTest extends WebTestCase
10{
11 protected function setUp()
12 {
13 $this->initClient([], $this->generateBasicAuthHeader());
14 }
15
16 public function testCreate()
17 {
18 $crawler = $this->client->request('GET', $this->getUrl('orocrm_task_create'));
19
20 $form = $crawler->selectButton('Save and Close')->form();
21 $form['orocrm_task[subject]'] = 'New task';
22 $form['orocrm_task[description]'] = 'New description';
23 $form['orocrm_task[dueDate]'] = '2014-03-04T20:00:00+0000';
24 $form['orocrm_task[owner]'] = '1';
25 $form['orocrm_task[reporter]'] = '1';
26
27 $this->client->followRedirects(true);
28 $crawler = $this->client->submit($form);
29 $result = $this->client->getResponse();
30 $this->assertHtmlResponseStatusCodeEquals($result, 200);
31 $this->assertContains('Task saved', $crawler->html());
32 }
33
34 /**
35 * @depends testCreate
36 */
37 public function testUpdate()
38 {
39 $response = $this->client->requestGrid(
40 'tasks-grid',
41 ['tasks-grid[_filter][reporterName][value]' => 'John Doe']
42 );
43
44 $result = $this->getJsonResponseContent($response, 200);
45 $result = reset($result['data']);
46
47 $crawler = $this->client->request(
48 'GET',
49 $this->getUrl('orocrm_task_update', ['id' => $result['id']])
50 );
51
52 $form = $crawler->selectButton('Save and Close')->form();
53 $form['orocrm_task[subject]'] = 'Task updated';
54 $form['orocrm_task[description]'] = 'Description updated';
55
56 $this->client->followRedirects(true);
57 $crawler = $this->client->submit($form);
58 $result = $this->client->getResponse();
59
60 $this->assertHtmlResponseStatusCodeEquals($result, 200);
61 $this->assertContains('Task saved', $crawler->html());
62 }
63
64 /**
65 * @depends testUpdate
66 */
67 public function testView()
68 {
69 $response = $this->client->requestGrid(
70 'tasks-grid',
71 ['tasks-grid[_filter][reporterName][value]' => 'John Doe']
72 );
73
74 $result = $this->getJsonResponseContent($response, 200);
75 $result = reset($result['data']);
76
77 $this->client->request(
78 'GET',
79 $this->getUrl('orocrm_task_view', ['id' => $result['id']])
80 );
81 $result = $this->client->getResponse();
82
83 $this->assertHtmlResponseStatusCodeEquals($result, 200);
84 $this->assertContains('Task updated - Tasks - Activities', $result->getContent());
85 }
86
87 /**
88 * @depends testUpdate
89 */
90 public function testIndex()
91 {
92 $this->client->request('GET', $this->getUrl('orocrm_task_index'));
93 $result = $this->client->getResponse();
94 $this->assertHtmlResponseStatusCodeEquals($result, 200);
95 $this->assertContains('Task updated', $result->getContent());
96 }
97}
Testing ACLs in a Controller¶
In this example, a user without sufficient permissions is trying to access
a controller action. The
Oro\Bundle\TestFrameworkBundle\Test\WebTestCase::assertHtmlResponseStatusCodeEquals
method is used to ensure that access to the requested resource is
denied for the user:
1// src/Oro/Bundle/UserBundle/Tests/Functional/UsersTest
2namespace Oro\Bundle\UserBundle\Tests\Functional;
3
4use Oro\Bundle\UserBundle\Tests\Functional\DataFixtures\LoadUserData;
5use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
6
7/**
8 * @outputBuffering enabled
9 */
10class UsersTest extends WebTestCase
11{
12 protected function setUp()
13 {
14 $this->initClient();
15 $this->loadFixtures(['Oro\Bundle\UserBundle\Tests\Functional\API\DataFixtures\LoadUserData']);
16 }
17
18 public function testUsersIndex()
19 {
20 $this->client->request(
21 'GET',
22 $this->getUrl('oro_user_index'),
23 [],
24 [],
25 $this->generateBasicAuthHeader(LoadUserData::USER_NAME, LoadUserData::USER_PASSWORD)
26 );
27 $result = $this->client->getResponse();
28 $this->assertHtmlResponseStatusCodeEquals($result, 403);
29 }
30
31 public function testGetUsersAPI()
32 {
33 $this->client->request(
34 'GET',
35 $this->getUrl('oro_api_get_users'),
36 ['limit' => 100],
37 [],
38 $this->generateWsseAuthHeader(LoadUserData::USER_NAME, LoadUserData::USER_API_KEY)
39 );
40 $result = $this->client->getResponse();
41 $this->assertJsonResponseStatusCodeEquals($result, 403);
42 }
43}
Here is an example of a fixture that adds a user without permissions:
1// src/Oro/Bundle/UserBundle/Tests/Functional/DataFixtures/LoadUserData.php
2namespace Oro\Bundle\UserBundle\Tests\Functional\DataFixtures;
3
4use Doctrine\Common\DataFixtures\AbstractFixture;
5use Doctrine\Common\Persistence\ObjectManager;
6
7use Symfony\Component\DependencyInjection\ContainerAwareInterface;
8use Symfony\Component\DependencyInjection\ContainerInterface;
9
10use Oro\Bundle\UserBundle\Entity\Role;
11use Oro\Bundle\UserBundle\Entity\UserApi;
12
13class LoadUserData extends AbstractFixture implements ContainerAwareInterface
14{
15 const USER_NAME = 'user_wo_permissions';
16 const USER_API_KEY = 'user_api_key';
17 const USER_PASSWORD = 'user_password';
18
19 private $container;
20
21 public function setContainer(ContainerInterface $container = null)
22 {
23 $this->container = $container;
24 }
25
26 public function load(ObjectManager $manager)
27 {
28 /** @var \Oro\Bundle\UserBundle\Entity\UserManager $userManager */
29 $userManager = $this->container->get('oro_user.manager');
30
31 // Find role for user to able to authenticate in test.
32 // You can use any available role that you want dependently on test logic.
33 $role = $manager->getRepository(Role::class)
34 ->findOneBy(['role' => 'IS_AUTHENTICATED_ANONYMOUSLY']);
35
36 // Creating new user
37 $user = $userManager->createUser();
38
39 // Creating API entity for user, we will reference it in testGetUsersAPI method,
40 // if you are not going to test API you can skip it
41 $api = new UserApi();
42 $api->setApiKey(self::USER_API_KEY)
43 ->setUser($user);
44
45 // Creating user
46 $user
47 ->setUsername(self::USER_NAME)
48 ->setPlainPassword(self::USER_PASSWORD) // This value is referenced in testUsersIndex method
49 ->setFirstName('Simple')
50 ->setLastName('User')
51 ->addRole($role)
52 ->setEmail('test@example.com')
53 ->setApi($api)
54 ->setSalt('');
55
56 // Handle password encoding
57 $userManager->updatePassword($user);
58
59 $manager->persist($user);
60 $manager->flush();
61 }
62}
Testing Commands¶
When OroPlatform is installed, you can test commands by using the
runCommand()
method from the Oro\Bundle\TestFrameworkBundle\Test\WebTestCase
class. This method executes a command with
given parameters and returns its output as a string. For example, see
what the test for the Oro\Bundle\SearchBundle\EventListener\UpdateSchemaDoctrineListener
class from the SearchBundle looks like:
1// src/Oro/Bundle/SearchBundle/Tests/Functional/EventListener/UpdateSchemaListenerTest.php
2namespace Oro\Bundle\SearchBundle\Tests\Functional\EventListener;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6class UpdateSchemaListenerTest extends WebTestCase
7{
8 protected function setUp()
9 {
10 $this->initClient();
11 }
12
13 /**
14 * @dataProvider commandOptionsProvider
15 */
16 public function testCommand($commandName, array $params, $expectedContent)
17 {
18 $result = $this->runCommand($commandName, $params);
19 $this->assertContains($expectedContent, $result);
20 }
21
22 public function commandOptionsProvider()
23 {
24 return [
25 'otherCommand' => [
26 'commandName' => 'doctrine:mapping:info',
27 'params' => [],
28 'expectedContent' => 'OK'
29 ],
30 'commandWithoutOption' => [
31 'commandName' => 'doctrine:schema:update',
32 'params' => [],
33 'expectedContent' => 'Please run the operation by passing one - or both - of the following options:'
34 ],
35 'commandWithAnotherOption' => [
36 'commandName' => 'doctrine:schema:update',
37 'params' => ['--dump-sql' => true],
38 'expectedContent' => 'ALTER TABLE'
39 ],
40 'commandWithForceOption' => [
41 'commandName' => 'doctrine:schema:update',
42 'params' => ['--force' => true],
43 'expectedContent' => 'Schema update and create index completed'
44 ]
45 ];
46 }
47}
See also
Read Testing Commands in the official documentation for more information on how to test commands in a Symfony application.
Testing Services or Repositories¶
To test services or repositories, you can access the service container through
the Oro\Bundle\TestFrameworkBundle\Test\WebTestCase::getContainer
method:
1// src/Oro/Bundle/FooBarBundle/Tests/Functional/FooBarTest.php
2namespace Oro\Bundle\FooBarBundle\Tests\Functional;
3
4use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
5
6class FooBarTest extends WebTestCase
7{
8 protected $repositoryOrService;
9
10 protected function setUp()
11 {
12 $this->initClient();
13 $this->loadFixtures(['Oro\Bundle\FooBarBundle\Tests\Functional\API\DataFixtures\LoadFooBarData']);
14 $this->repositoryOrService = $this->getContainer()->get('repository_or_service_id');
15 }
16
17 public function testMethod($commandName, array $params, $expectedContent)
18 {
19 $expected = 'test';
20 $this->assertEquals($expected, $this->repositoryOrService->callTestMethod());
21 }
22}
Functional Test Example¶
This is an example of how you can write an integration test for a class that uses Doctrine ORM without mocking its classes and using real Doctrine services:
1namespace Oro\Bundle\BatchBundle\Tests\Functional\ORM\QueryBuilder;
2
3use Doctrine\ORM\Query\Expr\Join;
4use Doctrine\ORM\QueryBuilder;
5use Doctrine\ORM\EntityManager;
6use Oro\Bundle\BatchBundle\ORM\QueryBuilder\CountQueryBuilderOptimizer;
7use Oro\Bundle\TestFrameworkBundle\Test\WebTestCase;
8
9class CountQueryBuilderOptimizerTest extends WebTestCase
10{
11 /**
12 * @dataProvider getCountQueryBuilderDataProvider
13 * @param QueryBuilder $queryBuilder
14 * @param string $expectedDql
15 */
16 public function testGetCountQueryBuilder(QueryBuilder $queryBuilder, $expectedDql)
17 {
18 $optimizer = new CountQueryBuilderOptimizer();
19 $countQb = $optimizer->getCountQueryBuilder($queryBuilder);
20 $this->assertInstanceOf('Doctrine\ORM\QueryBuilder', $countQb);
21 // Check for expected DQL
22 $this->assertEquals($expectedDql, $countQb->getQuery()->getDQL());
23 // Check that Optimized DQL can be converted to SQL
24 $this->assertNotEmpty($countQb->getQuery()->getSQL());
25 }
26
27 /**
28 * @return array
29 */
30 public function getCountQueryBuilderDataProvider()
31 {
32 self::initClient();
33 $em = self::getContainer()->get('doctrine.orm.entity_manager');
34
35 return [
36 'simple' => [
37 'queryBuilder' => self::createQueryBuilder($em)
38 ->from('OroUserBundle:User', 'u')
39 ->select(['u.id', 'u.username']),
40 'expectedDQL' => 'SELECT u.id FROM OroUserBundle:User u'
41 ],
42 'group_test' => [
43 'queryBuilder' => self::createQueryBuilder($em)
44 ->from('OroUserBundle:User', 'u')
45 ->select(['u.id', 'u.username as uName'])
46 ->groupBy('uName'),
47 'expectedDQL' => 'SELECT u.id, u.username as uName FROM OroUserBundle:User u GROUP BY uName'
48 ]
49 );
50 }
51
52 /**
53 * @param EntityManager $entityManager
54 * @return QueryBuilder
55 */
56 public static function createQueryBuilder(EntityManager $entityManager)
57 {
58 return new QueryBuilder($entityManager);
59 }
60}
Caution
If your class is responsible for retrieving data, it is better to load fixtures and retrieve them using a test class and then assert that the results are valid. Checking DQL is enough in this case because this it is the sole responsibility of this class to modify the query.