Squashing previous corrections into one commit along with a couple more

corrections to the docs, including changing the example seen in
fixtures.md
This commit is contained in:
Dan Brooks 2013-09-21 20:24:32 +01:00 committed by Dan Brooks
parent 9fa8945f2a
commit 3e5f788ddc
6 changed files with 236 additions and 198 deletions

View File

@ -15,23 +15,19 @@ The simple usage, Permission::check("PERM_CODE") will detect if the currently lo
**Group ACLs**
* Call **Permission::check("MY_PERMISSION_CODE")** to see if the current user has MY_PERMISSION_CODE.
* MY_PERMISSION_CODE can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
You can use whatever codes you like, but for the sanity of developers and users, it would be worth listing the codes in
[permissions:codes](/reference/permission)
* MY_PERMISSION_CODE can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
## PermissionProvider
`[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*. This method should return a
map of permission code names with a human readable explanation of its purpose (see
[permissions:codes](/reference/permission)).
`[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*.
This method should return a map of permission code names with a human readable explanation of its purpose.
:::php
class Page_Controller implements PermissionProvider {
public function init() {
if(!Permission::check("VIEW_SITE")) Security::permissionFailure();
}
public function providePermissions() {
return array(
"VIEW_SITE" => "Access the site",
@ -53,7 +49,7 @@ By default, permissions are used in the following way:
* If not logged in, the 'View' permissions must be 'anyone logged in' for a page to be displayed in a menu
* If logged in, you must be allowed to view a page for it to be displayed in a menu
**NOTE:** Should the canView() method on SiteTree be updated to call Permission::check("SITETREE_VIEW", $this->ID)?
**NOTE:** Should the canView() method on SiteTree be updated to call Permission::check("SITETREE_VIEW", $this->ID)?
Making this work well is a subtle business and should be discussed with a few developers.
## Setting up permissions

View File

@ -1,9 +1,11 @@
# Creating a SilverStripe Test
A test is created by extending one of two classes, SapphireTest and FunctionalTest. You would subclass SapphireTest to
test your application logic, for example testing the behaviour of one of your `[api:DataObjects]`, whereas FunctionalTest
is extended when you want to test your application's functionality, such as testing the results of GET and POST requests,
and validating the content of a page. `[api:FunctionalTest]` is a subclass of `[api:SapphireTest]`.
A test is created by extending one of two classes, SapphireTest and FunctionalTest.
You would subclass `[api:SapphireTest]` to test your application logic,
for example testing the behaviour of one of your `[DataObjects](api:DataObject)`,
whereas `[api:FunctionalTest]` is extended when you want to test your application's functionality,
such as testing the results of GET and POST requests,
and validating the content of a page. FunctionalTest is a subclass of SapphireTest.
## Creating a test from SapphireTest
@ -27,11 +29,7 @@ Here is an example of a test which extends SapphireTest:
'home' => 'home',
'staff' => 'my-staff',
'about' => 'about-us',
'staffduplicate' => 'my-staff-2',
'product1' => '1-1-test-product',
'product2' => 'another-product',
'product3' => 'another-product-2',
'product4' => 'another-product-3',
'staffduplicate' => 'my-staff-2'
);
foreach($expectedURLs as $fixture => $urlSegment) {
@ -44,11 +42,11 @@ Here is an example of a test which extends SapphireTest:
Firstly we define a static member `$fixture_file`, this should point to a file that represents the data we want to test,
represented in YAML. When our test is run, the data from this file will be loaded into a test database for our test to use.
This property can be an array of strings pointing to many .yml files, but for our test we are just using a string on its
own. For more detail on fixtures, see the [page on fixtures](fixtures).
own. For more detail on fixtures, see [this page](fixtures).
The second part of our class is the `testURLGeneration` method. This method is our test. You can asign many tests, but
again for our purposes there is just the one. When the test is executed, methods prefixed with the word **test** will be
run. The test database is rebuilt everytime one of these methods is run.
again for our purposes there is just the one. When the test is executed, methods prefixed with the word `test` will be
run. The test database is rebuilt every time one of these methods is run.
Inside our test method is the `objFromFixture` method that will generate an object for us based on data from our fixture
file. To identify to the object, we provide a class name and an identifier. The identifier is specified in the YAML file
@ -57,10 +55,10 @@ database. This means that you can use it to test the functions responsible for l
The final part of our test is an assertion command, `assertEquals`. An assertion command allows us to test for something
in our test methods (in this case we are testing if two values are equal). A test method can have more than one assertion
command, and if anyone of these tests fail, then the whole test method will fail.
command, and if any one of these assertions fail, so will the test method.
For more information on PHPUnit's assertions see the [PHPUnit manual](http://www.phpunit.de/manual/current/en/api.html#api.assert).
The `[api:SapphireTest]` class comes with additional assertions which are more specific to the Sapphire, for example the
`[assertEmailSent](api:SapphireTest->assertEmailSent())` method, which simulates sending emails through the `Email->send()`
API without actually using a mail server. For more details on this see th [testing emails](testing-email)) guide.
The `[api:SapphireTest]` class comes with additional assertions which are more specific to Sapphire, for example the
`assertEmailSent` method, which simulates sending emails through the `Email->send()`
API without actually using a mail server. For more details on this see the [testing emails](testing-email) guide.

View File

@ -2,177 +2,223 @@
## Overview
Often you need to test your functionality with some existing data, so called "fixtures".
The `[api:SapphireTest]` class already prepares an empty database for you,
and you have various ways to define those fixtures.
You will often find the need to test your functionality with some consistent data.
If we are testing our code with the same data each time,
we can trust our tests to yeild reliable results.
In Silverstripe we define this data via 'fixtures' (so called because of their fixed nature).
The `[api:SapphireTest]` class takes care of populating a test database with data from these fixtures -
all we have to do is define them, and we have a few ways in which we can do this.
## YAML Fixtures
YAML is a markup language which is deliberately simple and easy to read,
so ideal for our fixture generation.
so it is ideal for fixture generation.
We will begin with a sample file and talk our way through it.
Say we have the following two DataObjects:
Page:
home:
Title: Home
about:
Title: About Us
staff:
Title: Staff
URLSegment: my-staff
Parent: =>Page.about
RedirectorPage:
redirect_home:
RedirectionType: Internal
LinkTo: =>Page.home
:::php
class Player extends DataObject {
static $db = array (
'Name' => 'Varchar(255)'
);
static $has_one = array(
'Team' => 'Team'
);
}
The contents of the YAML file are broken into three levels.
class Team extends DataObject {
static $db = array (
'Name' => 'Varchar(255)',
'Origin' => 'Varchar(255)'
);
* **Top level: class names** - `Page` and `RedirectorPage`. This is the name of the dataobject class that should be created.
The fact that `RedirectorPage` is actually a subclass is irrelevant to the system populating the database. It just
instantiates the object you specify.
* **Second level: identifiers** - `home`, `about`, etc. These are the identifiers that you pass as
the second argument of SapphireTest::objFromFixture(). Each identifier you specify delimits a new database record.
This means that every record needs to have an identifier, whether you use it or not.
* **Third level: fields** - each field for the record is listed as a 3rd level entry. In most cases, the field's raw
content is provided. However, if you want to define a relationship, you can do so using "=>".
static $has_many = array(
'Players' => 'Player'
);
}
There are a couple of lines like this:
We can represent multiple instances of them in `YAML` as follows:
Parent: =>Page.about
:::yml
Player:
john:
Name: John
Team: =>Team.hurricanes
joe:
Name: Joe
Team: =>Team.crusaders
jack:
Name: Jack
Team: =>Team.crusaders
Team:
hurricanes:
Name: The Hurricanes
Origin: Wellington
crusaders:
Name: The Crusaders
Origin: Bay of Plenty
This will tell the system to set the ParentID database field to the ID of the Page object with the identifier "about".
This can be used on any has-one or many-many relationship. Note that we use the name of the relationship (Parent), and
not the name of the database field (ParentID)
Our `YAML` is broken up into three levels, signified by the indentation of each line.
In the first level of indentation, `Player` and `Team`,
represent the class names of the objects we want to be created for the test.
On many-many relationships, you should specify a comma separated list of values.
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers.
These are what you pass as the second argument of `SapphireTest::objFromFixture()`.
Each identifier you specify represents a new object.
MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3
The third and final level represents each individual object's fields.
A field can either be provided with raw data (such as the Names for our Players),
or we can define a relationship, as seen by the fields prefixed with `=>`.
An crucial thing to note is that **the YAML file specifies DataObjects, not database records**. The database is
populated by instantiating DataObject objects, setting the fields listed, and calling write(). This means that any
onBeforeWrite() or default value logic will be executed as part of the test. This forms the basis of our
testURLGeneration() test above.
Each one of our Players has a relationship to a Team,
this is shown with the `Team` field for each `Player` being set to `=>Team.` followed by a team name.
Take the player John for example, his team is the Hurricanes which is represented by `=>Team.hurricanes`.
This is tells the system that we want to set up a relationship for the `Player` object `john` with the `Team` object `hurricanes`.
It will populate the `Player` object's `TeamID` with the ID of `hurricanes`,
just like how a relationship is always set up.
For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff. When the
fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2.
<div class="hint" markdown='1'>
Note that we use the name of the relationship (Team), and not the name of the database field (TeamID).
</div>
Finally, be aware that requireDefaultRecords() is **not** called by the database populator - so you will need to specify
standard pages such as 404 and home in your YAML file.
This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship.
For `many-many` relationships, we specify a comma separated list of values.
For example we could just as easily write the above as:
:::yml
Player:
john:
Name: John
joe:
Name: Joe
jack:
Name: Jack
Team:
hurricanes:
Name: The Hurricanes
Origin: Wellington
Players: =>Player.john
crusaders:
Name: The Crusaders
Origin: Bay of Plenty
Players: =>Player.joe,=>Player.jack
A crucial thing to note is that **the YAML file specifies DataObjects, not database records**.
The database is populated by instantiating DataObject objects and setting the fields declared in the YML,
then calling write() on those objects.
This means that any `onBeforeWrite()` or default value logic will be executed as part of the test.
The reasoning behind this is to allow us to test the `onBeforeWrite` functionality of our objects.
You can see this kind of testing in action in the `testURLGeneration()` test from the example in
[Creating a SilverStripe Test](creating-a-silverstripe-test).
## Test Class Definition
### Manual Object Creation
## Manual Object Creation
Sometimes statically defined fixtures don't suffice, because of the complexity of the tested model,
or because the YAML format doesn't allow you to modify all model state.
Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model,
or because the YAML format doesn't allow you to modify all of a model's state.
One common example here is publishing pages (page fixtures aren't published by default).
You can always resort to creating objects manually in the test setup phase.
Since the test database is cleared on every test method, you'll get a fresh
set of test instances every time.
Since the test database is cleared on every test method, you'll get a fresh set of test instances every time.
:::php
class SiteTreeTest extends SapphireTest {
function setUp() {
parent::setUp();
:::php
class SiteTreeTest extends SapphireTest {
function setUp() {
parent::setUp();
for($i=0; $i<100; $i++) {
$page = new Page(array('Title' => "Page $i"));
$page->write();
$page->publish('Stage', 'Live');
}
}
}
for($i=0; $i<100; $i++) {
$page = new Page(array('Title' => "Page $i"));
$page->write();
$page->publish('Stage', 'Live');
}
}
}
## Fixture Factories
### Why Factories?
Manually defined fixture provide full flexibility, but very little in terms of structure and convention.
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you
to set default values, callbacks on object creation, and dynamic/lazy value setting.
By the way, the `SapphireTest` YAML fixtures rely on internally on this class as well.
While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention.
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values,
callbacks on object creation, and dynamic/lazy value setting.
The idea is that rather than instanciating objects directly, we'll have a factory class for them.
This factory can have so called "blueprints" defined on it, which tells the factory
how to instanciate an object of a specific type. Blueprints need a name,
which is usually set to the class it creates.
<div class="hint" markdown='1'>
SapphireTest uses FixtureFactory under the hood when it is provided with YAML based fixtures.
</div>
The idea is that rather than instantiating objects directly, we'll have a factory class for them.
This factory can have so called "blueprints" defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a name, which is usually set to the class it creates.
### Usage
Since blueprints are auto-created for all available DataObject subclasses,
you only need to instanciate a factory to start using it.
you only need to instantiate a factory to start using it.
:::php
$factory = Injector::inst()->create('FixtureFactory');
$obj = $factory->createObject('MyClass', 'myobj1');
:::php
$factory = Injector::inst()->create('FixtureFactory');
$obj = $factory->createObject('MyClass', 'myobj1');
It is important to remember that fixtures are referenced by arbitrary
identifiers ('myobj1'). These are internally mapped to their database identifiers.
:::
$databaseId = $factory->getId('MyClass', 'myobj1');
:::
$databaseId = $factory->getId('MyClass', 'myobj1');
In order to create an object with certain properties, just add a second argument:
:::php
$obj = $factory->createObject('MyClass', 'myobj1', array('MyProperty' => 'My Value'));
:::php
$obj = $factory->createObject('MyClass', 'myobj1', array('MyProperty' => 'My Value'));
### Default Properties
#### Default Properties
Blueprints can be overwritten in order to customize their behaviour,
for example with default properties in case none are passed into `createObject()`.
:::php
$factory->define('MyObject', array(
'MyProperty' => 'My Default Value'
));
:::php
$factory->define('MyObject', array(
'MyProperty' => 'My Default Value'
));
### Dependent Properties
#### Dependent Properties
Values can be set on demand through anonymous functions,
which can either generate random defaults, or create
composite values based on other fixture data.
Values can be set on demand through anonymous functions, which can either generate random defaults,
or create composite values based on other fixture data.
:::php
$factory->define('Member', array(
'Email' => function($obj, $data, $fixtures) {
if(isset($data['FirstName']) {
$obj->Email = strtolower($data['FirstName']) . '@example.org';
}
},
'Score' => function($obj, $data, $fixtures) {
$obj->Score = rand(0,10);
}
));
:::php
$factory->define('Member', array(
'Email' => function($obj, $data, $fixtures) {
if(isset($data['FirstName']) {
$obj->Email = strtolower($data['FirstName']) . '@example.org';
}
},
'Score' => function($obj, $data, $fixtures) {
$obj->Score = rand(0,10);
}
));
### Relations
#### Relations
Model relations can be expressed through the same notation as in the YAML fixture format
described earlier, through the `=>` prefix on data values.
:::php
$obj = $factory->createObject('MyObject', 'myobj1', array(
'MyHasManyRelation' => '=>MyOtherObject.obj1,=>MyOtherObject.obj2'
));
:::php
$obj = $factory->createObject('MyObject', 'myobj1', array(
'MyHasManyRelation' => '=>MyOtherObject.obj1,=>MyOtherObject.obj2'
));
### Callbacks
#### Callbacks
Sometimes new model instances need to be modified in ways which can't be expressed
in their properties, for example to publish a page, which requires a method call.
:::php
$blueprint = Injector::inst()->create('FixtureBlueprint', 'Member');
$blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
$obj->publish('Stage', 'Live');
});
$page = $factory->define('Page', $blueprint);
:::php
$blueprint = Injector::inst()->create('FixtureBlueprint', 'Member');
$blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
$obj->publish('Stage', 'Live');
});
$page = $factory->define('Page', $blueprint);
Available callbacks:
@ -186,43 +232,43 @@ CMS admins could both inherit from the `Member` class, but have completely
different properties. This is where named blueprints come in.
By default, blueprint names equal the class names they manage.
:::php
$memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member');
$adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member');
$adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
if(isset($fixtures['Group']['admin'])) {
$adminGroup = Group::get()->byId($fixtures['Group']['admin']);
$obj->Groups()->add($adminGroup);
}
});
$member = $factory->createObject('Member'); // not in admin group
$admin = $factory->createObject('AdminMember'); // in admin group
:::php
$memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member');
$adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member');
$adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
if(isset($fixtures['Group']['admin'])) {
$adminGroup = Group::get()->byId($fixtures['Group']['admin']);
$obj->Groups()->add($adminGroup);
}
});
$member = $factory->createObject('Member'); // not in admin group
$admin = $factory->createObject('AdminMember'); // in admin group
### Full Test Example
:::php
class MyObjectTest extends SapphireTest {
:::php
class MyObjectTest extends SapphireTest {
protected $factory;
protected $factory;
function __construct() {
parent::__construct();
function __construct() {
parent::__construct();
$factory = Injector::inst()->create('FixtureFactory');
// Defines a "blueprint" for new objects
$factory->define('MyObject', array(
'MyProperty' => 'My Default Value'
));
$this->factory = $factory;
}
$factory = Injector::inst()->create('FixtureFactory');
// Defines a "blueprint" for new objects
$factory->define('MyObject', array(
'MyProperty' => 'My Default Value'
));
$this->factory = $factory;
}
function testSomething() {
$MyObjectObj = $this->factory->createObject(
'MyObject',
array('MyOtherProperty' => 'My Custom Value')
);
// $myPageObj->MyProperty = My Default Value
// $myPageObj->MyOtherProperty = My Custom Value
}
}
function testSomething() {
$MyObjectObj = $this->factory->createObject(
'MyObject',
array('MyOtherProperty' => 'My Custom Value')
);
// $myPageObj->MyProperty = My Default Value
// $myPageObj->MyOtherProperty = My Custom Value
}
}

View File

@ -31,7 +31,7 @@ test.
**Fake Object**: A substitute object that simply replaces a real object with the same interface, and returns a
pre-determined (usually fixed) value from each method.
**Mock Object:** A substitute object that mimicks the same behavior as a real object (some people think of mocks as
**Mock Object:** A substitute object that mimics the same behavior as a real object (some people think of mocks as
"crash test dummy" objects). Mocks differ from other kinds of substitute objects in that they must understand the
context of each call to them, setting expectations of which, and what order, methods will be invoked and what parameters
will be passed.

View File

@ -111,7 +111,7 @@ All command-line arguments are documented on
### Via the "sake" Wrapper on Command Line
The [sake](/topics/commandline) executable that comes with SilverStripe can trigger a customized
"[api:TestRunner]" class that handles the PHPUnit configuration and output formatting.
`[api:TestRunner]` class that handles the PHPUnit configuration and output formatting.
While the custom test runner a handy tool, its also more limited than using `phpunit` directly,
particularly around formatting test output.

View File

@ -1,48 +1,46 @@
# Testing Email
SilverStripe's test system has built-in support for testing emails sent using the Email class.
SilverStripe's test system has built-in support for testing emails sent using the `[api:Email]` class.
## How it works
For this to work, you need to send emails using the `Email` class, which is generally the way that we recommend you
send emails in your SilverStripe application. Here is a simple example of how you might do this:
For this to work, you need to send emails using the `Email` class,
which is generally the way that we recommend you send emails in your SilverStripe application.
Here is a simple example of how you might do this:
:::php
$e = new Email();
$e->To = "someone@example.com";
$e->Subject = "Hi there";
$e->Body = "I just really wanted to email you and say hi.";
$e->send();
:::php
$e = new Email();
$e->To = "someone@example.com";
$e->Subject = "Hi there";
$e->Body = "I just really wanted to email you and say hi.";
$e->send();
Normally, the `send()` method would send an email using PHP's `mail()` function.
However, if you are running a `[api:SapphireTest]` test, then it holds off actually sending the email,
and instead lets you assert that an email was sent using this method.
Normally, the send() method would send an email using PHP's mail() function. However, if you are running a `[api:SapphireTest]`
test, then it holds off actually sending the email, and instead lets you assert that an email was sent using this method.
:::php
$this->assertEmailSent("someone@example.com", null, "/th.*e$/");
:::php
$this->assertEmailSent("someone@example.com", null, "/th.*e$/");
The arguments are `$to`, `$from`, `$subject`, `$body`, and can take one of the three following types:
The arguments are `$to`, `$from`, `$subject`, `$body`, and can be take one of the following three forms:
* A string: match exactly that string
* `null/false`: match anything
* A PERL regular expression (starting with '/'): match that regular expression
* A string: match exactly that string
* `null/false`: match anything
* A PERL regular expression (starting with '/'): match that regular expression
## How to use it
Given all of that, there is not a lot that you have to do in order to test emailing functionality in your application.
* Write your SilverStripe application, using the Email class to send emails.
* Write tests that trigger the email sending functionality.
* Include appropriate `$this->assertEmailSent()` calls in those tests.
Whenever we include e-mailing functionality in our application,
we simply use `$this->assertEmailSent()` to check our mail has been passed to PHP `mail` in our tests.
That's it!
## What isn't tested
It's important to realise that this email testing doesn't actually test everything that there is to do with email. The
focus of this email testing system is testing that your application is triggering emails correctly. It doesn't test
your email infrastructure outside of the webserver. For example:
It's important to realise that this email testing doesn't actually test everything that there is to do with email.
The focus of this email testing system is testing that your application is triggering emails correctly.
It doesn't test your email infrastructure outside of the webserver. For example:
* It won't test that email is correctly configured on your webserver
* It won't test whether your emails are going to be lost in someone's spam filter
@ -50,9 +48,9 @@ your email infrastructure outside of the webserver. For example:
## How it's built
For those of you who want to dig a little deeper, here's a quick run-through of how the system has been built. As well
as explaining how we built the email test, this is a good design pattern for making other "tricky external systems"
testable:
For those of you who want to dig a little deeper, here's a quick run-through of how the system has been built.
As well as explaining how we built the email test,
this is a good design pattern for making other "tricky external systems" testable:
1. The `Email::send()` method makes uses of a static object, `Email::$mailer`, to do the dirty work of calling
mail(). The default mailer is an object of type `Mailer`, which performs a normal send.