Merge pull request #2443 from dangerdan/doc_corrections

Documentation corrections
This commit is contained in:
Ingo Schommer 2013-09-24 11:45:53 -07:00
commit 3edbfa5b99
6 changed files with 236 additions and 198 deletions

View File

@ -17,14 +17,10 @@ The simple usage, Permission::check("PERM_CODE") will detect if the currently lo
* Call **Permission::check("MY_PERMISSION_CODE")** to see if the current user has MY_PERMISSION_CODE. * 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. * 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)
## PermissionProvider ## PermissionProvider
`[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*. This method should return a `[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*.
map of permission code names with a human readable explanation of its purpose (see This method should return a map of permission code names with a human readable explanation of its purpose.
[permissions:codes](/reference/permission)).
:::php :::php
class Page_Controller implements PermissionProvider { class Page_Controller implements PermissionProvider {

View File

@ -1,9 +1,11 @@
# Creating a SilverStripe Test # Creating a SilverStripe Test
A test is created by extending one of two classes, SapphireTest and FunctionalTest. You would subclass SapphireTest to A test is created by extending one of two classes, SapphireTest and FunctionalTest.
test your application logic, for example testing the behaviour of one of your `[api:DataObjects]`, whereas FunctionalTest You would subclass `[api:SapphireTest]` to test your application logic,
is extended when you want to test your application's functionality, such as testing the results of GET and POST requests, for example testing the behaviour of one of your `[DataObjects](api:DataObject)`,
and validating the content of a page. `[api:FunctionalTest]` is a subclass of `[api:SapphireTest]`. 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 ## Creating a test from SapphireTest
@ -27,11 +29,7 @@ Here is an example of a test which extends SapphireTest:
'home' => 'home', 'home' => 'home',
'staff' => 'my-staff', 'staff' => 'my-staff',
'about' => 'about-us', 'about' => 'about-us',
'staffduplicate' => 'my-staff-2', 'staffduplicate' => 'my-staff-2'
'product1' => '1-1-test-product',
'product2' => 'another-product',
'product3' => 'another-product-2',
'product4' => 'another-product-3',
); );
foreach($expectedURLs as $fixture => $urlSegment) { foreach($expectedURLs as $fixture => $urlSegment) {
@ -44,10 +42,10 @@ 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, 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. 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 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 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 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. 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 Inside our test method is the `objFromFixture` method that will generate an object for us based on data from our fixture
@ -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 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 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). 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 The `[api:SapphireTest]` class comes with additional assertions which are more specific to Sapphire, for example the
`[assertEmailSent](api:SapphireTest->assertEmailSent())` method, which simulates sending emails through the `Email->send()` `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. API without actually using a mail server. For more details on this see the [testing emails](testing-email) guide.

View File

@ -2,80 +2,126 @@
## Overview ## Overview
Often you need to test your functionality with some existing data, so called "fixtures". You will often find the need to test your functionality with some consistent data.
The `[api:SapphireTest]` class already prepares an empty database for you, If we are testing our code with the same data each time,
and you have various ways to define those fixtures. 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 Fixtures
YAML is a markup language which is deliberately simple and easy to read, 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: :::php
home: class Player extends DataObject {
Title: Home static $db = array (
about: 'Name' => 'Varchar(255)'
Title: About Us );
staff:
Title: Staff
URLSegment: my-staff
Parent: =>Page.about
RedirectorPage: static $has_one = array(
redirect_home: 'Team' => 'Team'
RedirectionType: Internal );
LinkTo: =>Page.home }
class Team extends DataObject {
static $db = array (
'Name' => 'Varchar(255)',
'Origin' => 'Varchar(255)'
);
The contents of the YAML file are broken into three levels. static $has_many = array(
'Players' => 'Player'
);
}
* **Top level: class names** - `Page` and `RedirectorPage`. This is the name of the dataobject class that should be created. We can represent multiple instances of them in `YAML` as follows:
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 "=>".
There are a couple of lines like this: :::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
Parent: =>Page.about 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.
This will tell the system to set the ParentID database field to the ID of the Page object with the identifier "about". The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers.
This can be used on any has-one or many-many relationship. Note that we use the name of the relationship (Parent), and These are what you pass as the second argument of `SapphireTest::objFromFixture()`.
not the name of the database field (ParentID) Each identifier you specify represents a new object.
On many-many relationships, you should specify a comma separated list of values. 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 `=>`.
MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3 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.
An crucial thing to note is that **the YAML file specifies DataObjects, not database records**. The database is <div class="hint" markdown='1'>
populated by instantiating DataObject objects, setting the fields listed, and calling write(). This means that any Note that we use the name of the relationship (Team), and not the name of the database field (TeamID).
onBeforeWrite() or default value logic will be executed as part of the test. This forms the basis of our </div>
testURLGeneration() test above.
For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff. When the This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship.
fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2. For `many-many` relationships, we specify a comma separated list of values.
For example we could just as easily write the above as:
Finally, be aware that requireDefaultRecords() is **not** called by the database populator - so you will need to specify :::yml
standard pages such as 404 and home in your YAML file. 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 ## Test Class Definition
### Manual Object Creation
Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model,
## Manual Object Creation or because the YAML format doesn't allow you to modify all of a model's state.
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.
One common example here is publishing pages (page fixtures aren't published by default). 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. 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 Since the test database is cleared on every test method, you'll get a fresh set of test instances every time.
set of test instances every time.
:::php :::php
class SiteTreeTest extends SapphireTest { class SiteTreeTest extends SapphireTest {
@ -94,20 +140,21 @@ set of test instances every time.
### Why Factories? ### Why Factories?
Manually defined fixture provide full flexibility, but very little in terms of structure and convention. 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 Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values,
to set default values, callbacks on object creation, and dynamic/lazy value setting. callbacks on object creation, and dynamic/lazy value setting.
By the way, the `SapphireTest` YAML fixtures rely on internally on this class as well.
The idea is that rather than instanciating objects directly, we'll have a factory class for them. <div class="hint" markdown='1'>
This factory can have so called "blueprints" defined on it, which tells the factory SapphireTest uses FixtureFactory under the hood when it is provided with YAML based fixtures.
how to instanciate an object of a specific type. Blueprints need a name, </div>
which is usually set to the class it creates.
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 ### Usage
Since blueprints are auto-created for all available DataObject subclasses, 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 :::php
$factory = Injector::inst()->create('FixtureFactory'); $factory = Injector::inst()->create('FixtureFactory');
@ -124,7 +171,7 @@ In order to create an object with certain properties, just add a second argument
:::php :::php
$obj = $factory->createObject('MyClass', 'myobj1', array('MyProperty' => 'My Value')); $obj = $factory->createObject('MyClass', 'myobj1', array('MyProperty' => 'My Value'));
### Default Properties #### Default Properties
Blueprints can be overwritten in order to customize their behaviour, Blueprints can be overwritten in order to customize their behaviour,
for example with default properties in case none are passed into `createObject()`. for example with default properties in case none are passed into `createObject()`.
@ -134,11 +181,10 @@ for example with default properties in case none are passed into `createObject()
'MyProperty' => 'My Default Value' 'MyProperty' => 'My Default Value'
)); ));
### Dependent Properties #### Dependent Properties
Values can be set on demand through anonymous functions, Values can be set on demand through anonymous functions, which can either generate random defaults,
which can either generate random defaults, or create or create composite values based on other fixture data.
composite values based on other fixture data.
:::php :::php
$factory->define('Member', array( $factory->define('Member', array(
@ -152,7 +198,7 @@ composite values based on other fixture data.
} }
)); ));
### Relations #### Relations
Model relations can be expressed through the same notation as in the YAML fixture format Model relations can be expressed through the same notation as in the YAML fixture format
described earlier, through the `=>` prefix on data values. described earlier, through the `=>` prefix on data values.
@ -162,7 +208,7 @@ described earlier, through the `=>` prefix on data values.
'MyHasManyRelation' => '=>MyOtherObject.obj1,=>MyOtherObject.obj2' 'MyHasManyRelation' => '=>MyOtherObject.obj1,=>MyOtherObject.obj2'
)); ));
### Callbacks #### Callbacks
Sometimes new model instances need to be modified in ways which can't be expressed 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. in their properties, for example to publish a page, which requires a method call.

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 **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. 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 "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 context of each call to them, setting expectations of which, and what order, methods will be invoked and what parameters
will be passed. will be passed.

View File

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

View File

@ -1,11 +1,12 @@
# Testing Email # 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 ## 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 For this to work, you need to send emails using the `Email` class,
send emails in your SilverStripe application. Here is a simple example of how you might do this: 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 :::php
$e = new Email(); $e = new Email();
@ -14,15 +15,14 @@ send emails in your SilverStripe application. Here is a simple example of how y
$e->Body = "I just really wanted to email you and say hi."; $e->Body = "I just really wanted to email you and say hi.";
$e->send(); $e->send();
Normally, the `send()` method would send an email using PHP's `mail()` function.
Normally, the send() method would send an email using PHP's mail() function. However, if you are running a `[api:SapphireTest]` However, if you are running a `[api:SapphireTest]` test, then it holds off actually sending the email,
test, then it holds off actually sending the email, and instead lets you assert that an email was sent using this method. and instead lets you assert that an email was sent using this method.
:::php :::php
$this->assertEmailSent("someone@example.com", null, "/th.*e$/"); $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 * A string: match exactly that string
* `null/false`: match anything * `null/false`: match anything
@ -31,18 +31,16 @@ The arguments are `$to`, `$from`, `$subject`, `$body`, and can be take one of th
## How to use it ## 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. Given all of that, there is not a lot that you have to do in order to test emailing functionality in your application.
Whenever we include e-mailing functionality in our application,
* Write your SilverStripe application, using the Email class to send emails. we simply use `$this->assertEmailSent()` to check our mail has been passed to PHP `mail` in our tests.
* Write tests that trigger the email sending functionality.
* Include appropriate `$this->assertEmailSent()` calls in those tests.
That's it! That's it!
## What isn't tested ## 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 It's important to realise that this email testing doesn't actually test everything that there is to do with email.
focus of this email testing system is testing that your application is triggering emails correctly. It doesn't test The focus of this email testing system is testing that your application is triggering emails correctly.
your email infrastructure outside of the webserver. For example: 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 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 * 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 ## 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 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 explaining how we built the email test, this is a good design pattern for making other "tricky external systems" As well as explaining how we built the email test,
testable: 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 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. mail(). The default mailer is an object of type `Mailer`, which performs a normal send.