How to Unit Test your Field Types¶
Caution
This page is not updated for Rollerworks v2.0 yet.
A Field consists of 3 core object: a field type (implementing
FieldType
) the
SearchField
and the
SearchFieldView
.
The only class that is usually manipulated by programmers is the field type class
which serves as a field blueprint. It is used to generate the SearchField
and the
SearchFieldView
. You could test it directly by mocking its interactions with the
factory but it would be complex. It is better to pass it to SearchFactory like it
is done in a real application. It is simple to bootstrap and you can trust
the Search components enough to use them as a testing base.
There is already a class that you can benefit from for simple FieldTypes
testing: SearchIntegrationTestCase
. It is used to
test the core types and you can use it to test your types too.
Note
Depending on the way you installed RollerworksSearch the tests may
not be downloaded. Use the --prefer-source
option with
Composer if this is the case.
The Basics¶
The simplest SearchIntegrationTestCase
implementation looks like the following:
// src/Acme/Invoice/Tests/Search/Type/InvoiceNumberTypeTest.php
namespace Acme\Invoice\Tests\Search\Type;
use Rollerworks\Component\Search\Test\SearchIntegrationTestCase;
use Acme\Invoice\Search\Type\InvoiceNumberType;
use Acme\Invoice\Search\ValueComparison\InvoiceNumberComparison;
use Acme\Invoice\InvoiceNumber;
class InvoiceNumberTypeTest extends SearchIntegrationTestCase
{
public function testValidInvoiceNumber()
{
$field = $this->getFactory()->createField('invoice', 'invoice_number');
$expectedOutput = new InvoiceNumber(2015, 20);
$expectedView = '2015-0020';
$this->assertTransformedEquals($field, $expectedOutput, '2015-0020', $expectedView);
$this->assertTransformedEquals($field, $expectedOutput, '2015-020', $expectedView);
$this->assertTransformedEquals($field, $expectedOutput, '2015-20', $expectedView);
}
public function testWrongInputFails()
{
$field = $this->getFactory()->createField('invoice', 'invoice_number');
$this->assertTransformedFails($field, '201-0020');
$this->assertTransformedFails($field, '2015-');
$this->assertTransformedFails($field, '201500');
}
protected function getTypes()
{
return array(
new InvoiceNumberType(
new InvoiceNumberComparison()
)
);
}
}
So, what does it test? Here comes a detailed explanation.
First you verify if the FieldType
compiles. This includes basic class
inheritance, the buildField
function and options resolution. This should
be the first test you write:
$type = new TestedType();
$form = $this->getFactory()->create($type);
This test checks that none of your data transformers used by the field
failed. The assertTransformedEquals
checks that the value-input is transformed
properly to the expected output and that the reverse transforming is what you
expect:
$this->assertTransformedEquals($field, $expectedOutput, '2015-0020', $expectedView);
$this->assertTransformedEquals($field, $expectedOutput, '2015-020', $expectedView);
$this->assertTransformedEquals($field, $expectedOutput, '2015-20', $expectedView);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
Note
The expected view result is not required, but its a good practice to ensure the field transformers work properly.
Next, verify that invalid values are not transformed:
$this->assertTransformedFails($field, '201-0020');
Caution
Make sure to only call getFactory
method and not use the private
factory
property to get the factory.
To access the factory builder (before calling the getFactory
method)
use the factoryBuilder
property.
Adding a Type your Type Depends on¶
Your field type may depend on other types that are not registered by default. It might look like this:
// src/Acme/Invoice/Search/Type/TestedType.php
// ... the getParent method
return 'my_custom_type';
To create your type correctly, you need to make the other type available
to the search factory in your test. The easiest way is to register it manually
before creating the child type using the getTypes
method:
// src/Acme/Test/Tests/Search/Type/TestedTypeTest.php
namespace Acme\Test\Tests\Search\Type;
use Rollerworks\Component\Search\Test\SearchIntegrationTestCase;
use Acme\Test\Search\Type\ParentType;
use Acme\Test\Search\Type\TestedType;
use Acme\Test\ValueObject;
class TestedTypeTest extends SearchIntegrationTestCase
{
public function testValidValueTransforms()
{
$field = $this->getFactory()->createField('field_name', 'tested_type');
$expectedOutput = new ValueObject(10, 20, 50);
$expectedView = '{10, 20, 50}';
$this->assertTransformedEquals($field, $expectedOutput, '{10, 20,50}', $expectedView);
}
protected function getTypes()
{
return array(
new ParentType(),
new TestedType(),
);
}
}
Caution
Make sure the parent type you add is well tested. Otherwise you may be getting errors that are not related to the type you are currently testing but to its children.
Adding custom Extensions¶
It often happens that you use some options that are added by
type extensions. One of the
cases may be the Symfony ValidatorExtension
with its constraints
option.
The SearchIntegrationTestCase
loads only the core form extension so an “Invalid option”
exception will be raised if you try to use it for testing a class that depends
on other extensions. You need add those extensions to the factory object:
// src/Acme/Test/Tests/Search/Type/TestedTypeTest.php
namespace Acme\Test\Tests\Search\Type;
use Rollerworks\Component\Search\Test\SearchIntegrationTestCase;
use Rollerworks\Component\Search\Extension\Symfony\ValidatorExtension;
class TestedTypeTest extends SearchIntegrationTestCase
{
protected function getTypeExtensions()
{
return array(
new ValidatorExtension(),
);
}
// ... your tests
}
Note
The Symfony ValidatorExtension
class is provided by a separate package.
See Installing the Library for more information to install this extension.
Testing against different Sets of Data¶
If you are not familiar yet with PHPUnit’s data providers, this might be a good opportunity to use them:
// src/Acme/Test/Tests/Search/Type/TestedTypeTest.php
namespace Acme\Test\Tests\Search\Type;
use Rollerworks\Component\Search\Test\SearchIntegrationTestCase;
use Acme\Test\Search\Type\TestedType;
use Acme\Test\ValueObject;
class TestedTypeTest extends SearchIntegrationTestCase
{
protected function getTypes()
{
return array(
new TestedType(),
);
}
/**
* @dataProvider getValidTestData
*/
public function testValidDataTransforms($input, $expected, $viewExpected = null)
{
$field = $this->getFactory()->createField('field_name', 'tested_type');
$this->assertTransformedEquals($field, $expectedOutput, $input, $expectedView);
}
public function getValidTestData()
{
return array(
array('{10, 20,50}', new ValueObject(10, 20, 50), '{10, 20, 50}'),
array('{10, 20, 50}', new ValueObject(10, 20, 50), '{10, 20, 50}'),
array('{10,20,50}', new ValueObject(10, 20, 50), '{10, 20, 50}'),
);
}
}
The code above will run your test three times with 3 different sets of data. This allows for decoupling the test fixtures from the tests and directly testing against multiple sets of data.