mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Review and rewrites for Testing Developer Guide
This commit is contained in:
parent
4fcbc9a402
commit
576f34a539
@ -1,5 +1,7 @@
|
||||
# Page Types
|
||||
|
||||
Hi people
|
||||
|
||||
## Introduction
|
||||
|
||||
Page Types are the basic building blocks of any SilverStripe website. A page type can define:
|
||||
|
@ -5,4 +5,4 @@ summary: Understand the ways to modify the built-in functionality through Extens
|
||||
|
||||
## How-to
|
||||
|
||||
[CHILDREN How_To]
|
||||
[CHILDREN Folder=How_To]
|
269
docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md
Normal file
269
docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md
Normal file
@ -0,0 +1,269 @@
|
||||
title: Unit and Integration Testing
|
||||
summary: Test models, database logic and your object methods.
|
||||
|
||||
# Unit and Integration Testing
|
||||
|
||||
A Unit Test is an automated piece of code that invokes a unit of work in the application and then checks the behavior
|
||||
to ensure that it works as it should. A simple example would be to test the result of a PHP method.
|
||||
|
||||
**mysite/code/Page.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Page extends SiteTree {
|
||||
|
||||
public static function MyMethod() {
|
||||
return (1 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
**mysite/tests/PageTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PageTest extends SapphireTest {
|
||||
|
||||
public function testMyMethod() {
|
||||
$this->assertEquals(2, Page::MyMethod());
|
||||
}
|
||||
}
|
||||
|
||||
<div class="info" markdown="1">
|
||||
Tests for your application should be stored in the `mysite/tests` directory. Test cases for add-ons should be stored in
|
||||
the `(modulename)/tests` directory.
|
||||
|
||||
Test case classes should end with `Test` (e.g PageTest) and test methods must start with `test` (e.g testMyMethod).
|
||||
</div>
|
||||
|
||||
A SilverStripe unit test is created by extending one of two classes, [api:SapphireTest] or [api:FunctionalTest].
|
||||
|
||||
[api:SapphireTest] is used to test your model logic (such as a `DataObject`), and [api:FunctionalTest] is used when
|
||||
you want to test a `Controller`, `Form` or anything that requires a web page.
|
||||
|
||||
<div class="info" markdown="1">
|
||||
`FunctionalTest` is a subclass of `SapphireTest` so will inherit all of the behaviors. By subclassing `FunctionalTest`
|
||||
you gain the ability to load and test web pages on the site.
|
||||
|
||||
`SapphireTest` in turn, extends `PHPUnit_Framework_TestCase`. For more information on `PHPUnit_Framework_TestCase` see
|
||||
the [PHPUnit](http://www.phpunit.de) documentation. It provides a lot of fundamental concepts that we build on in this
|
||||
documentation.
|
||||
</div>
|
||||
|
||||
## Running Tests
|
||||
|
||||
### PHPUnit Binary
|
||||
|
||||
The `phpunit` binary should be used from the root directory of your website.
|
||||
|
||||
:::bash
|
||||
phpunit
|
||||
# Runs all tests
|
||||
|
||||
phpunit framework/tests/
|
||||
# Run all tests of a specific module
|
||||
|
||||
phpunit framework/tests/filesystem
|
||||
# Run specific tests within a specific module
|
||||
|
||||
phpunit framework/tests/filesystem/FolderTest.php
|
||||
# Run a specific test
|
||||
|
||||
phpunit framework/tests '' flush=all
|
||||
# Run tests with optional `$_GET` parameters (you need an empty second argument)
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
If phpunit is not installed globally on your machine, you may need to replace the above usage of `phpunit` with the full
|
||||
path (e.g `vendor/bin/phpunit framework/tests`)
|
||||
</div>
|
||||
|
||||
<div class="info" markdown="1">
|
||||
All command-line arguments are documented on [phpunit.de](http://www.phpunit.de/manual/current/en/textui.html).
|
||||
</div>
|
||||
|
||||
### Via a Web Browser
|
||||
|
||||
Executing tests from the command line is recommended, since it most closely reflects test runs in any automated testing
|
||||
environments. If for some reason you don't have access to the command line, you can also run tests through the browser.
|
||||
|
||||
http://yoursite.com/dev/tests
|
||||
|
||||
|
||||
### Via the CLI
|
||||
|
||||
The [sake](../cli) executable that comes with SilverStripe can trigger a customized `[api:TestRunner]` class that
|
||||
handles the PHPUnit configuration and output formatting. While the custom test runner a handy tool, it's also more
|
||||
limited than using `phpunit` directly, particularly around formatting test output.
|
||||
|
||||
:::bash
|
||||
sake dev/tests/all
|
||||
# Run all tests
|
||||
|
||||
sake dev/tests/module/framework,cms
|
||||
# Run all tests of a specific module (comma-separated)
|
||||
|
||||
sake dev/tests/FolderTest,OtherTest
|
||||
# Run specific tests (comma-separated)
|
||||
|
||||
sake dev/tests/all "flush=all&foo=bar"
|
||||
# Run tests with optional `$_GET` parameters
|
||||
|
||||
sake dev/tests/all SkipTests=MySkippedTest
|
||||
# Skip some tests
|
||||
|
||||
|
||||
## Test Databases and Fixtures
|
||||
|
||||
SilverStripe tests create their own database when the test starts. New `ss_tmp` databases are created using the same
|
||||
connection details you provide for the main website. The new `ss_tmp` database does not copy what is currently in your
|
||||
application database. To provide seed data use a [Fixture](fixtures) file.
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
As the test runner will create new databases for the tests to run, the database user should have the appropriate
|
||||
permissions to create new databases on your server.
|
||||
</div>
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
The test database is rebuilt every time one of the test methods is run. Over time, you may have several hundred test
|
||||
databases on your machine. To get rid of them is a call to `http://yoursite.com/dev/tests/cleanupdb`
|
||||
</div>
|
||||
|
||||
## Custom PHPUnit Configuration
|
||||
|
||||
The `phpunit` executable can be configured by command line arguments or through an XML file. SilverStripe comes with a
|
||||
default `phpunit.xml.dist` that you can use as a starting point. Copy the file into `phpunit.xml` and customize to your
|
||||
needs.
|
||||
|
||||
**phpunit.xml**
|
||||
|
||||
:::xml
|
||||
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true">
|
||||
<testsuite name="Default">
|
||||
<directory>mysite/tests</directory>
|
||||
<directory>cms/tests</directory>
|
||||
<directory>framework/tests</directory>
|
||||
</testsuite>
|
||||
|
||||
<listeners>
|
||||
<listener class="SS_TestListener" file="framework/dev/TestListener.php" />
|
||||
</listeners>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>sanitychecks</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
|
||||
<div class="alert" markdown="1">
|
||||
This configuration file doesn't apply for running tests through the "sake" wrapper
|
||||
</div>
|
||||
|
||||
|
||||
### setUp() and tearDown()
|
||||
|
||||
In addition to loading data through a [Fixture File](fixtures), a test case may require some additional setup work to be
|
||||
run before each test method. For this, use the PHPUnit `setUp` and `tearDown` methods. These are run at the start and
|
||||
end of each test.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PageTest extends SapphireTest {
|
||||
|
||||
function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// create 100 pages
|
||||
for($i=0; $i<100; $i++) {
|
||||
$page = new Page(array('Title' => "Page $i"));
|
||||
$page->write();
|
||||
$page->publish('Stage', 'Live');
|
||||
}
|
||||
|
||||
// reset configuration for the test.
|
||||
Config::nest();
|
||||
Config::inst()->update('Foo', 'bar', 'Hello!');
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
// restores the config variables
|
||||
Config::unnest();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testMyMethod() {
|
||||
// ..
|
||||
}
|
||||
|
||||
public function testMySecondMethod() {
|
||||
// ..
|
||||
}
|
||||
}
|
||||
|
||||
`tearDownOnce` and `setUpOnce` can be used to run code just once for the file rather than before and after each
|
||||
individual test case.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PageTest extends SapphireTest {
|
||||
|
||||
function setUpOnce() {
|
||||
parent::setUpOnce();
|
||||
|
||||
// ..
|
||||
}
|
||||
|
||||
public function tearDownOnce() {
|
||||
parent::tearDownOnce();
|
||||
|
||||
// ..
|
||||
}
|
||||
}
|
||||
|
||||
## Generating a Coverage Report
|
||||
|
||||
PHPUnit can generate a code coverage report ([docs](http://www.phpunit.de/manual/current/en/code-coverage-analysis.html))
|
||||
by executing the following commands.
|
||||
|
||||
:::bash
|
||||
phpunit --coverage-html assets/coverage-report
|
||||
# Generate coverage report for the whole project
|
||||
|
||||
phpunit --coverage-html assets/coverage-report mysite/tests/
|
||||
# Generate coverage report for the "mysite" module
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
These commands will output a report to the `assets/coverage-report/` folder. To view the report, open the `index.html`
|
||||
file within a web browser.
|
||||
</div>
|
||||
|
||||
Typically, only your own custom PHP code in your project should be regarded when producing these reports. To exclude
|
||||
some `thirdparty/` directories add the following to the `phpunit.xml` configuration file.
|
||||
|
||||
:::xml
|
||||
<filter>
|
||||
<blacklist>
|
||||
<directory suffix=".php">framework/dev/</directory>
|
||||
<directory suffix=".php">framework/thirdparty/</directory>
|
||||
<directory suffix=".php">cms/thirdparty/</directory>
|
||||
|
||||
<!-- Add your custom rules here -->
|
||||
<directory suffix=".php">mysite/thirdparty/</directory>
|
||||
</blacklist>
|
||||
</filter>
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [How to Write a SapphireTest](how_tos/write_a_sapphiretest)
|
||||
* [How to Write a FunctionalTest](how_tos/write_a_functionaltest)
|
||||
* [Fixtures](fixtures)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:TestRunner]
|
||||
* [api:SapphireTest]
|
||||
* [api:FunctionalTest]
|
@ -1,93 +0,0 @@
|
||||
# Why Unit Test?
|
||||
|
||||
*Note: This is part of the [SilverStripe Testing Guide](/topics/testing/).*
|
||||
|
||||
So at this point, you might be thinking, *"that's way too complicated, I don't have time to write unit tests on top of
|
||||
all the things I'm already doing"*. Fair enough. But, maybe you're already doing things that are close to unit testing
|
||||
without realizing it. Everyone tests all the time, in various ways. Even if you're just refreshing a URL in a browser to
|
||||
review the context of your changes, you're testing!
|
||||
|
||||
First, ask yourself how much time you're already spending debugging your code. Are you inserting `echo`, `print_r`,
|
||||
and `die` statements into different parts of your program and watching the details dumping out to screen? **Yes, you
|
||||
know you are.** So how much time do you spend doing this? How much of your development cycle is dependent on dumping out
|
||||
the contents of variables to confirm your assumptions about what they contain?
|
||||
|
||||
From this position, it may seem that unit testing may take longer and have uncertain outcomes simply because it involves
|
||||
adding more code. You'd be right, in the sense that we should be striving to write as little code as possible on
|
||||
projects. The more code there is, the more room there is for bugs, the harder it is to maintain. There's absolutely no
|
||||
doubt about that. But if you're dumping the contents of variables out to the screen, you are already making assertions
|
||||
about your code. All unit testing does is separate these assertions into separate runnable blocks of code, rather than
|
||||
have them scattered inline with your actual program logic.
|
||||
|
||||
The practical and immediate advantages of unit testing are twofold. Firstly, they mean you don't have to mix your
|
||||
debugging and analysis code in with your actual program code (with the need to delete, or comment it out once you're
|
||||
done). Secondly, they give you a way to capture the questions you ask about your code while you're writing it, and the
|
||||
ability to run those questions over and over again, with no overhead or interference from other parts of the system.
|
||||
|
||||
Unit testing becomes particularly useful when exploring boundary conditions or edge case behavior of your code. You can
|
||||
write assertions that verify examples of how your methods will be called, and verify that they always return the right
|
||||
results each time. If you make changes that have the potential to break these expected results, running the unit tests
|
||||
over and over again will give you immediate feedback of any regressions.
|
||||
|
||||
Unit tests also function as specifications. They are a sure way to describe an API and how it works by simply running
|
||||
the code and demonstrating what parameters each method call expects and each method call returns. You could think of it
|
||||
as live API documentation that provides real-time information about how the code works.
|
||||
|
||||
Unit test assertions are best understood as **pass/fail** statements about the behavior of your code. Ideally, you want
|
||||
every assertion to pass, and this is usually up by the visual metaphor of green/red signals. When things are all green,
|
||||
it's all good. Red indicates failure, and provides a direct warning that you need to fix or change your code.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Everyone has a different set of ideas about what makes good code, and particular preferences towards a certain style of
|
||||
logic. At the same time, frameworks and programming languages provide clear conventions and design idioms that guide
|
||||
code towards a certain common style.
|
||||
|
||||
If all this ranting and raving about the importance of testing hasn't made got you thinking that you want to write tests
|
||||
then we haven't done our job well enough! But the key question still remains - *"where do I start?"*.
|
||||
|
||||
To turn the key in the lock and answer this question, we need to look at how automated testing fits into the different
|
||||
aspects of the SilverStripe platform. There are some significant differences in goals and focus between different layers
|
||||
of the system and interactions between the core, and various supporting modules.
|
||||
|
||||
### SilverStripe Core
|
||||
|
||||
In open source core development, we are focussing on a large and (for the most part) stable system with existing well
|
||||
defined behavior. Our overarching goal is that we do not want to break or change this existing behavior, but at the same
|
||||
time we want to extend and improve it.
|
||||
|
||||
Testing the SilverStripe framework should focus on [characterization](http://en.wikipedia.org/wiki/Characterization_Test).
|
||||
We should be writing tests that illustrate the way that the API works, feeding commonly used methods with a range of
|
||||
inputs and states and verifying that these methods respond with clear and predictable results.
|
||||
|
||||
Especially important is documenting and straighten out edge case behavior, by pushing various objects into corners and
|
||||
twisting them into situations that we know are likely to manifest with the framework in the large.
|
||||
|
||||
### SilverStripe Modules
|
||||
|
||||
Modules usually encapsulate a smaller, and well defined subset of behavior or special features added on top of the core
|
||||
platform. A well constructed module will contain a reference suite of unit tests that documents and verifies all the
|
||||
basic aspects of the module design. See also: [modules](/topics/modules).
|
||||
|
||||
### Project Modules
|
||||
|
||||
Testing focus on client projects will not be quite so straightforward. Every project involves different personalities,
|
||||
goals, and priorities, and most of the time, there is simply not enough time or resources to exhaustively predicate
|
||||
every granular aspect of an application.
|
||||
|
||||
On application projects, the best option is to keep tests lean and agile. Most useful is a focus on experimentation and
|
||||
prototyping, using the testing framework to explore solution spaces and bounce new code up into a state where we can be
|
||||
happy that it works the way we want it to.
|
||||
|
||||
## Rules of Thumb
|
||||
|
||||
**Be aware of breaking existing behavior.** Run your full suite of tests every time you do a commit.
|
||||
|
||||
**Not everything is permanent.** If a test is no longer relevant, delete it from the repository.
|
||||
|
||||
## See Also
|
||||
|
||||
* [Getting to Grips with SilverStripe
|
||||
Testing](http://www.slideshare.net/maetl/getting-to-grips-with-silverstripe-testing)
|
||||
|
||||
|
106
docs/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md
Normal file
106
docs/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md
Normal file
@ -0,0 +1,106 @@
|
||||
title: Functional Testing
|
||||
summary: Test controllers, forms and HTTP responses.
|
||||
|
||||
# Functional Testing
|
||||
|
||||
[api:FunctionalTest] test your applications `Controller` logic and anything else which requires a web request. The
|
||||
core idea of these tests is the same as `SapphireTest` unit tests but `FunctionalTest` adds several methods for
|
||||
creating [api:SS_HTTPRequest], receiving [api:SS_HTTPResponse] objects and modifying the current user session.
|
||||
|
||||
## Get
|
||||
|
||||
:::php
|
||||
$page = $this->get($url);
|
||||
|
||||
Performs a GET request on $url and retrieves the [api:SS_HTTPResponse]. This also changes the current page to the value
|
||||
of the response.
|
||||
|
||||
## Post
|
||||
|
||||
:::php
|
||||
$page = $this->post($url);
|
||||
|
||||
Performs a POST request on $url and retrieves the [api:SS_HTTPResponse]. This also changes the current page to the value
|
||||
of the response.
|
||||
|
||||
## Submit
|
||||
|
||||
:::php
|
||||
$submit = $this->submitForm($formID, $button = null, $data = array());
|
||||
|
||||
Submits the given form (`#ContactForm`) on the current page and returns the [api:SS_HTTPResponse].
|
||||
|
||||
## LogInAs
|
||||
|
||||
:::php
|
||||
$this->logInAs($member);
|
||||
|
||||
Logs a given user in, sets the current session. To log all users out pass `null` to the method.
|
||||
|
||||
:::php
|
||||
$this->logInAs(null);
|
||||
|
||||
## Assertions
|
||||
|
||||
The `FunctionalTest` class also provides additional asserts to validate your tests.
|
||||
|
||||
### assertPartialMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertPartialMatchBySelector('p.good',array(
|
||||
'Test save was successful'
|
||||
));
|
||||
|
||||
Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The
|
||||
assertion fails if one of the expectedMatches fails to appear.
|
||||
|
||||
|
||||
### assertExactMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertExactMatchBySelector("#MyForm_ID p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
|
||||
Asserts that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The
|
||||
assertion fails if one of the expectedMatches fails to appear.
|
||||
|
||||
### assertPartialHTMLMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertPartialHTMLMatchBySelector("#MyForm_ID p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
|
||||
Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The content of every matching tag will be examined. The
|
||||
assertion fails if one of the expectedMatches fails to appear.
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
`&nbsp;` characters are stripped from the content; make sure that your assertions take this into account.
|
||||
</div>
|
||||
|
||||
### assertExactHTMLMatchBySelector
|
||||
|
||||
:::php
|
||||
$this->assertExactHTMLMatchBySelector("#MyForm_ID p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
|
||||
Assert that the most recently queried page contains a number of content tags specified by a CSS selector. The given CSS
|
||||
selector will be applied to the HTML of the most recent page. The full HTML of every matching tag will be examined. The
|
||||
assertion fails if one of the expectedMatches fails to appear.
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
`&nbsp;` characters are stripped from the content; make sure that your assertions take this into account.
|
||||
</div>
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [How to write a FunctionalTest](how_tos/write_a_functionaltest)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:FunctionalTest]
|
@ -1,110 +0,0 @@
|
||||
# Configure PHPUnit for your project
|
||||
|
||||
This guide helps you to run [PHPUnit](http://phpunit.de) tests in your SilverStripe project.
|
||||
See "[Testing](/topics/testing)" for an overview on how to create unit tests.
|
||||
|
||||
## Coverage reports
|
||||
|
||||
PHPUnit can generate code coverage reports for you ([docs](http://www.phpunit.de/manual/current/en/code-coverage-analysis.html)):
|
||||
|
||||
* `phpunit --coverage-html assets/coverage-report`: Generate coverage report for the whole project
|
||||
* `phpunit --coverage-html assets/coverage-report mysite/tests/`: Generate coverage report for the "mysite" module
|
||||
|
||||
Typically, only your own custom PHP code in your project should be regarded when
|
||||
producing these reports. Here's how you would exclude some `thirdparty/` directories:
|
||||
|
||||
<filter>
|
||||
<blacklist>
|
||||
<directory suffix=".php">framework/dev/</directory>
|
||||
<directory suffix=".php">framework/thirdparty/</directory>
|
||||
<directory suffix=".php">cms/thirdparty/</directory>
|
||||
|
||||
<!-- Add your custom rules here -->
|
||||
<directory suffix=".php">mysite/thirdparty/</directory>
|
||||
</blacklist>
|
||||
</filter>
|
||||
|
||||
## Running unit and functional tests separately
|
||||
|
||||
You can use the filesystem structure of your unit tests to split
|
||||
different aspects. In the simplest form, you can limit your test exeuction
|
||||
to a specific directory by passing in a directory argument (`phpunit mymodule/tests`).
|
||||
To specify multiple directories, you have to use the XML configuration file.
|
||||
This can be useful to only run certain parts of your project
|
||||
on continous integration, or produce coverage reports separately
|
||||
for unit and functional tests.
|
||||
|
||||
Example `phpunit-unittests-only.xml`:
|
||||
|
||||
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite>
|
||||
<directory>mysite/tests/unit</directory>
|
||||
<directory>othermodule/tests/unit</directory>
|
||||
<!-- ... -->
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<!-- ... -->
|
||||
</phpunit>
|
||||
|
||||
You can run with this XML configuration simply by invoking `phpunit --configuration phpunit-unittests-only.xml`.
|
||||
|
||||
The same effect can be achieved with the `--group` argument and some PHPDoc (see [phpunit.de](http://www.phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.groups)).
|
||||
|
||||
## Speeding up your test execution with the SQLite3 module
|
||||
|
||||
Test execution can easily take a couple of minutes for a full run,
|
||||
particularly if you have a lot of database write operations.
|
||||
This is a problem when you're trying to to "[Test Driven Development](http://en.wikipedia.org/wiki/Test-driven_development)".
|
||||
|
||||
To speed things up a bit, you can simply use a faster database just for executing tests.
|
||||
The SilverStripe database layer makes this relatively easy, most likely
|
||||
you won't need to adjust any project code or alter SQL statements.
|
||||
|
||||
The [SQLite3 module](http://www.silverstripe.org/sqlite-database/) provides an interface
|
||||
to a very fast database that requires minimal setup and is fully file-based.
|
||||
It should give you up to 4x speed improvements over running tests in MySQL or other
|
||||
more "heavy duty" relational databases.
|
||||
|
||||
Example `mysite/_config.php`:
|
||||
|
||||
// Customized configuration for running with different database settings.
|
||||
// Ensure this code comes after ConfigureFromEnv.php
|
||||
if(Director::isDev()) {
|
||||
if(isset($_GET['db']) && ($db = $_GET['db'])) {
|
||||
global $databaseConfig;
|
||||
if($db == 'sqlite3') $databaseConfig['type'] = 'SQLite3Database';
|
||||
}
|
||||
}
|
||||
|
||||
You can either use the database on a single invocation:
|
||||
|
||||
phpunit framework/tests "" db=sqlite3
|
||||
|
||||
or through a `<php>` flag in your `phpunit.xml` (see [Appenix C: "Setting PHP INI settings"](http://www.phpunit.de/manual/current/en/appendixes.configuration.html)):
|
||||
|
||||
<phpunit>
|
||||
<!-- ... -->
|
||||
<php>
|
||||
<get name="db" value="sqlite3"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
Note that on every test run, the manifest is flushed to avoid any bugs where a test doesn't clean up after
|
||||
itself properly. You can override that behaviour by passing `flush=0` to the test command:
|
||||
|
||||
phpunit framework/tests flush=0
|
||||
|
||||
Alternatively, you can set the var in your `phpunit.xml` file:
|
||||
|
||||
<phpunit>
|
||||
<!-- ... -->
|
||||
<php>
|
||||
<get name="flush" value="0"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
<div class="hint" markdown="1">
|
||||
It is recommended that you still run your tests with the original database driver (at least on continuous integration)
|
||||
to ensure a realistic test scenario.
|
||||
</div>
|
@ -0,0 +1,7 @@
|
||||
title: Behavior Testing
|
||||
summary: Describe how your application should behave in plain text and run tests in a browser.
|
||||
|
||||
# Behavior Testing
|
||||
|
||||
For behavior testing in SilverStripe, check out
|
||||
[SilverStripe Behat Documentation](https://github.com/silverstripe-labs/silverstripe-behat-extension/).
|
@ -1,64 +0,0 @@
|
||||
# Creating a SilverStripe Test
|
||||
|
||||
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
|
||||
|
||||
Here is an example of a test which extends SapphireTest:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class SiteTreeTest extends SapphireTest {
|
||||
|
||||
// Define the fixture file to use for this test class
|
||||
protected static $fixture_file = 'SiteTreeTest.yml';
|
||||
|
||||
/**
|
||||
* Test generation of the URLSegment values.
|
||||
* - Turns things into lowercase-hyphen-format
|
||||
* - Generates from Title by default, unless URLSegment is explicitly set
|
||||
* - Resolves duplicates by appending a number
|
||||
*/
|
||||
public function testURLGeneration() {
|
||||
$expectedURLs = array(
|
||||
'home' => 'home',
|
||||
'staff' => 'my-staff',
|
||||
'about' => 'about-us',
|
||||
'staffduplicate' => 'my-staff-2'
|
||||
);
|
||||
|
||||
foreach($expectedURLs as $fixture => $urlSegment) {
|
||||
$obj = $this->objFromFixture('Page', $fixture);
|
||||
$this->assertEquals($urlSegment, $obj->URLSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 [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 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
|
||||
but not saved in the database anywhere, `objFromFixture` looks the `[api:DataObject]` up in memory rather than using the
|
||||
database. This means that you can use it to test the functions responsible for looking up content in the database.
|
||||
|
||||
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 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 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.
|
@ -1,69 +0,0 @@
|
||||
# Creating a functional tests
|
||||
|
||||
Functional tests test your controllers. The core of these are the same as unit tests:
|
||||
|
||||
* Create a subclass of `[api:SapphireTest]` in the `mysite/tests` or `(module)/tests` folder.
|
||||
* Define static $fixture_file to point to a database YAML file.
|
||||
* Create methods that start with "test" to create your tests.
|
||||
* Assertions are used to work out if a test passed or failed.
|
||||
|
||||
The code of the tests is a little different. Instead of examining the behaviour of objects, we example the results of
|
||||
URLs. Here is an example from the subsites module:
|
||||
|
||||
:::php
|
||||
class SubsiteAdminTest extends SapphireTest {
|
||||
private static $fixture_file = 'subsites/tests/SubsiteTest.yml';
|
||||
|
||||
/**
|
||||
* Return a session that has a user logged in as an administrator
|
||||
*/
|
||||
public function adminLoggedInSession() {
|
||||
return Injector::inst()->create('Session', array(
|
||||
'loggedInAs' => $this->idFromFixture('Member', 'admin')
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test generation of the view
|
||||
*/
|
||||
public function testBasicView() {
|
||||
// Open the admin area logged in as admin
|
||||
$response1 = Director::test('admin/subsites/', null, $this->adminLoggedInSession());
|
||||
|
||||
// Confirm that this URL gets you the entire page, with the edit form loaded
|
||||
$response2 = Director::test('admin/subsites/show/1', null, $this->adminLoggedInSession());
|
||||
$this->assertTrue(strpos($response2->getBody(), 'id="Root_Configuration"') !== false);
|
||||
$this->assertTrue(strpos($response2->getBody(), '<head') !== false);
|
||||
|
||||
// Confirm that this URL gets you just the form content, with the edit form loaded
|
||||
$response3 = Director::test('admin/subsites/show/1', array('ajax' => 1), $this->adminLoggedInSession());
|
||||
|
||||
$this->assertTrue(strpos($response3->getBody(), 'id="Root_Configuration"') !== false);
|
||||
$this->assertTrue(strpos($response3->getBody(), '<form') === false);
|
||||
$this->assertTrue(strpos($response3->getBody(), '<head') === false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
We are using a new static method here: **Director::test($url, $postVars, $sessionObj)**
|
||||
|
||||
Director::test() lets us execute a URL and see what happens. It bypasses HTTP, instead relying on the cleanly
|
||||
encapsulated execution model of `[api:Controller]`.
|
||||
|
||||
It takes 3 arguments:
|
||||
|
||||
* $url: The URL to execute
|
||||
* $postVars: Post variables to pass to the URL
|
||||
* $sessionObj: A Session object representing the current session.
|
||||
|
||||
And it returns an `[api:HTTPResponse]` object, which will give you the response headers (including redirection), status code,
|
||||
and body.
|
||||
|
||||
We can use string processing on the body of the response to then see if it fits with our expectations.
|
||||
|
||||
If you're testing for natural language responses like error messages, make sure to use [i18n](/topics/i18n) translations through
|
||||
the *_t()* method to avoid tests failing when i18n is enabled.
|
||||
|
||||
Note that for a more highlevel testing approach, SilverStripe also supports
|
||||
[behaviour-driven testing through Behat](https://github.com/silverstripe-labs/silverstripe-behat-extension). It interacts
|
||||
directly with your website or CMS interface by remote controlling an actual browser, driven by natural language assertions.
|
@ -1,24 +1,20 @@
|
||||
title: Fixtures
|
||||
summary: Populate test databases with fake seed data.
|
||||
|
||||
# Fixtures
|
||||
|
||||
## Overview
|
||||
To test functionality correctly, we must use consistent data. If we are testing our code with the same data each
|
||||
time, we can trust our tests to yield reliable results and to identify when the logic changes. Each test run in
|
||||
SilverStripe starts with a fresh database containing no records. `Fixtures` provide a way to describe the initial data
|
||||
to load into the database. The `[api:SapphireTest]` class takes care of populating a test database with data from
|
||||
fixtures - all we have to do is define them.
|
||||
|
||||
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 yield 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 it
|
||||
is ideal for fixture generation.
|
||||
|
||||
Say we have the following two DataObjects:
|
||||
Fixtures are defined in `YAML`. `YAML` is a markup language which is deliberately simple and easy to read, so it is
|
||||
ideal for fixture generation. Say we have the following two DataObjects:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class Player extends DataObject {
|
||||
|
||||
private static $db = array (
|
||||
@ -44,6 +40,8 @@ Say we have the following two DataObjects:
|
||||
|
||||
We can represent multiple instances of them in `YAML` as follows:
|
||||
|
||||
**mysite/tests/fixtures.yml**
|
||||
|
||||
:::yml
|
||||
Player:
|
||||
john:
|
||||
@ -63,43 +61,36 @@ We can represent multiple instances of them in `YAML` as follows:
|
||||
Name: The Crusaders
|
||||
Origin: Bay of Plenty
|
||||
|
||||
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 `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.
|
||||
|
||||
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.
|
||||
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are **identifiers**. Each identifier you specify
|
||||
represents a new object and can be referenced in the PHP using `objFromFixture`
|
||||
|
||||
:::php
|
||||
$player = $this->objFromFixture('Player', 'jack');
|
||||
|
||||
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
|
||||
`=>`.
|
||||
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 `=>`.
|
||||
|
||||
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.
|
||||
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.
|
||||
<div class="info" markdown="1">
|
||||
Take the player John in our example YAML, his team is the Hurricanes which is represented by `=>Team.hurricanes`. This
|
||||
sets the `has_one` relationship for John with with the `Team` object `hurricanes`.
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
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.
|
||||
This style of relationship declaration can be used for any type of relationship (i.e `has_one`, `has_many`, `many_many`).
|
||||
|
||||
For example we could just as easily write the above as:
|
||||
We can also declare the relationships conversely. Another way we could write the previous example is:
|
||||
|
||||
:::yml
|
||||
Player:
|
||||
@ -111,31 +102,37 @@ For example we could just as easily write the above as:
|
||||
Name: Jack
|
||||
Team:
|
||||
hurricanes:
|
||||
Name: The Hurricanes
|
||||
Name: Hurricanes
|
||||
Origin: Wellington
|
||||
Players: =>Player.john
|
||||
crusaders:
|
||||
Name: The Crusaders
|
||||
Name: 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 `YAML`, then
|
||||
calling `write()` on those objects. Take for instance the `hurricances` record in the `YAML`. It is equivalent to
|
||||
writing:
|
||||
|
||||
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.
|
||||
:::php
|
||||
$team = new Team(array(
|
||||
'Name' => 'Hurricanes',
|
||||
'Origin' => 'Wellington'
|
||||
));
|
||||
|
||||
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).
|
||||
$team->write();
|
||||
|
||||
$team->Players()->add($john);
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value logic will be executed as part of the
|
||||
test.
|
||||
</div>
|
||||
|
||||
### Defining many_many_extraFields
|
||||
|
||||
`many_many` relations can have additional database fields attached to the
|
||||
relationship. For example we may want to declare the role each player has in the
|
||||
team.
|
||||
`many_many` relations can have additional database fields attached to the relationship. For example we may want to
|
||||
declare the role each player has in the team.
|
||||
|
||||
:::php
|
||||
class Player extends DataObject {
|
||||
@ -166,7 +163,7 @@ team.
|
||||
);
|
||||
}
|
||||
|
||||
To provide the value for the many_many_extraField use the YAML list syntax.
|
||||
To provide the value for the `many_many_extraField` use the YAML list syntax.
|
||||
|
||||
:::yml
|
||||
Player:
|
||||
@ -191,89 +188,61 @@ To provide the value for the many_many_extraField use the YAML list syntax.
|
||||
- =>Player.jack:
|
||||
Role: Winger
|
||||
|
||||
## Test Class Definition
|
||||
|
||||
### Manual Object Creation
|
||||
|
||||
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.
|
||||
|
||||
:::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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## Fixture Factories
|
||||
|
||||
### Why Factories?
|
||||
While manually defined fixtures provide full flexibility, they offer 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 to set default values, callbacks
|
||||
on object creation, and dynamic/lazy value setting.
|
||||
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values, callbacks on object
|
||||
creation, and dynamic/lazy value setting.
|
||||
|
||||
<div class="hint" markdown='1'>
|
||||
SapphireTest uses FixtureFactory under the hood when it is provided with YAML
|
||||
based fixtures.
|
||||
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.
|
||||
The idea is that rather than instantiating objects directly, we'll have a factory class for them. This factory can have
|
||||
*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 such as `Member` or `Page`.
|
||||
|
||||
### Usage
|
||||
|
||||
Since blueprints are auto-created for all available DataObject subclasses,
|
||||
you only need to instantiate a factory to start using it.
|
||||
Blueprints are auto-created for all available DataObject subclasses, you only need to instantiate a factory to start
|
||||
using them.
|
||||
|
||||
:::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.
|
||||
$obj = $factory->createObject('Team', 'hurricanes');
|
||||
|
||||
In order to create an object with certain properties, just add a third argument:
|
||||
|
||||
:::php
|
||||
$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'));
|
||||
|
||||
#### 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'
|
||||
$obj = $factory->createObject('Team', 'hurricanes', array(
|
||||
'Name' => 'My Value'
|
||||
));
|
||||
|
||||
#### Dependent Properties
|
||||
<div class="warning" markdown="1">
|
||||
It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally
|
||||
mapped to their database identifiers.
|
||||
</div>
|
||||
|
||||
Values can be set on demand through anonymous functions, which can either generate random defaults,
|
||||
or create composite values based on other fixture data.
|
||||
After we've created this object in the factory, `getId` is used to retrieve it by the identifier.
|
||||
|
||||
:::php
|
||||
$databaseId = $factory->getId('Team', 'hurricanes');
|
||||
|
||||
|
||||
### Default Properties
|
||||
|
||||
Blueprints can be overwritten in order to customize their behavior. For example, if a Fixture does not provide a Team
|
||||
name, we can set the default to be `Unknown Team`.
|
||||
|
||||
:::php
|
||||
$factory->define('Team', array(
|
||||
'Name' => 'Unknown Team'
|
||||
));
|
||||
|
||||
### 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.
|
||||
|
||||
:::php
|
||||
$factory->define('Member', array(
|
||||
@ -287,26 +256,28 @@ or create composite values based on other fixture data.
|
||||
}
|
||||
));
|
||||
|
||||
#### 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.
|
||||
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'
|
||||
$obj = $factory->createObject('Team', 'hurricanes', array(
|
||||
'MyHasManyRelation' => '=>Player.john,=>Player.joe'
|
||||
));
|
||||
|
||||
#### 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.
|
||||
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);
|
||||
|
||||
Available callbacks:
|
||||
@ -316,14 +287,15 @@ Available callbacks:
|
||||
|
||||
### Multiple Blueprints
|
||||
|
||||
Data of the same type can have variations, for example forum members vs.
|
||||
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.
|
||||
Data of the same type can have variations, for example forum members vs. 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']);
|
||||
@ -332,32 +304,15 @@ By default, blueprint names equal the class names they manage.
|
||||
});
|
||||
|
||||
$member = $factory->createObject('Member'); // not in admin group
|
||||
|
||||
$admin = $factory->createObject('AdminMember'); // in admin group
|
||||
|
||||
### Full Test Example
|
||||
## Related Documentation
|
||||
|
||||
:::php
|
||||
class MyObjectTest extends SapphireTest {
|
||||
* [How to use a FixtureFactory](how_to/fixturefactories/)
|
||||
|
||||
protected $factory;
|
||||
## API Documentation
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
* [api:FixtureFactory]
|
||||
* [api:FixtureBlueprint]
|
||||
|
||||
$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
|
||||
}
|
||||
}
|
||||
|
@ -1,61 +0,0 @@
|
||||
# Unit Test Troubleshooting
|
||||
|
||||
Part of the [SilverStripe Testing Guide](testing-guide).
|
||||
|
||||
## I can't run my new test class
|
||||
|
||||
If you've just added a test class, but you can't see it via the web interface, chances are, you haven't flushed your
|
||||
manifest cache - append `?flush=1` to the end of your URL querystring.
|
||||
|
||||
## Class 'PHPUnit_Framework_MockObject_Generator' not found
|
||||
|
||||
This is due to an upgrade in PHPUnit 3.5 which PEAR doesn't handle correctly.<br>
|
||||
It can be fixed by running the following commands:
|
||||
|
||||
pear install -f phpunit/DbUnit
|
||||
pear install -f phpunit/PHPUnit_MockObject
|
||||
pear install -f phpunit/PHPUnit_Selenium
|
||||
|
||||
## My tests fail seemingly random when comparing database IDs
|
||||
|
||||
When defining fixtures in the YML format, you only assign aliases
|
||||
for them, not direct database IDs. Even if you insert only one record
|
||||
on a clean database, it is not guaranteed to produce ID=1 on every run.
|
||||
So to make your tests more robust, use the aliases rather than hardcoded IDs.
|
||||
|
||||
Also, some databases don't return records in a consistent sort order
|
||||
unless you explicitly tell them to. If you don't want to test sort order
|
||||
but rather just the returned collection,
|
||||
|
||||
:::php
|
||||
$myPage = $this->objFromFixture('Page', 'mypage');
|
||||
$myOtherPage = $this->objFromFixture('Page', 'myotherpage');
|
||||
$pages = Page::get();
|
||||
// Bad: Assumptions about IDs and their order
|
||||
$this->assertEquals(array(1,2), $pages->column('ID'));
|
||||
// Good: Uses actually created IDs, independent of their order
|
||||
$this->assertContains($myPage->ID, $pages->column('ID'));
|
||||
$this->assertContains($myOtherPage->ID, $pages->column('ID'));
|
||||
|
||||
## My fixtures are getting complicated, how do I inspect their database state?
|
||||
|
||||
Fixtures are great because they're easy to define through YML,
|
||||
but sometimes can be a bit of a blackbox when it comes to the actual
|
||||
database state they create. These are temporary databases, which are
|
||||
destructed directly after the test run - which is intentional,
|
||||
but not very helpful if you want to verify that your fixtures have been created correctly.
|
||||
|
||||
SilverStripe comes with a URL action called `dev/tests/startsession`.
|
||||
When called through a web browser, it prompts for a fixture file
|
||||
which it creates a new database for, and sets it as the current database
|
||||
in this browser session until you call `dev/tests/endsession`.
|
||||
|
||||
For more advanced users, you can also have a look in the `[api:YamlFixture]`
|
||||
class to see what's going on behind the scenes.
|
||||
|
||||
## My database server is cluttered with `tmpdb...` databases
|
||||
|
||||
This is a common problem due to aborted test runs,
|
||||
which don't clean up after themselves correctly
|
||||
(mostly because of a fatal PHP error in the tests).
|
||||
The easiest way to get rid of them is a call to `dev/tests/cleanupdb`.
|
@ -1,46 +0,0 @@
|
||||
# Glossary
|
||||
|
||||
**Assertion:** A predicate statement that must be true when a test runs.
|
||||
|
||||
**Behat:** A behaviour-driven testing library used with SilverStripe as a higher-level
|
||||
alternative to the `FunctionalTest` API, see [http://behat.org](http://behat.org).
|
||||
|
||||
**Test Case:** The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the
|
||||
base test case.
|
||||
|
||||
**Test Suite:** Also known as a 'test group', a composite of test cases, used to collect individual unit tests into
|
||||
packages, allowing all tests to be run at once.
|
||||
|
||||
**Fixture:** Usually refers to the runtime context of a unit test - the environment and data prerequisites that must be
|
||||
in place in order to run the test and expect a particular outcome. Most unit test frameworks provide methods that can be
|
||||
used to create fixtures for the duration of a test - `setUp` - and clean them up after the test is done - `tearDown'.
|
||||
|
||||
**Refactoring:** A behavior preserving transformation of code. If you change the code, while keeping the actual
|
||||
functionality the same, it is refactoring. If you change the behavior or add new functionality it's not.
|
||||
|
||||
**Smell:** A code smell is a symptom of a problem. Usually refers to code that is structured in a way that will lead to
|
||||
problems with maintenance or understanding.
|
||||
|
||||
**Spike:** A limited and throwaway sketch of code or experiment to get a feel for how long it will take to implement a
|
||||
certain feature, or a possible direction for how that feature might work.
|
||||
|
||||
**Test Double:** Also known as a 'Substitute'. A general term for a dummy object that replaces a real object with the
|
||||
same interface. Substituting objects is useful when a real object is difficult or impossible to incorporate into a unit
|
||||
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 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.
|
||||
|
||||
**Test-Driven Development (TDD):** A style of programming where tests for a new feature are constructed before any code
|
||||
is written. Code to implement the feature is then written with the aim of making the tests pass. Testing is used to
|
||||
understand the problem space and discover suitable APIs for performing specific actions.
|
||||
|
||||
**Behavior Driven Development (BDD):** An extension of the test-driven programming style, where tests are used primarily
|
||||
for describing the specification of how code should perform. In practice, there's little or no technical difference - it
|
||||
all comes down to language. In BDD, the usual terminology is changed to reflect this change of focus, so *Specification*
|
||||
is used in place of *Test Case*, and *should* is used in place of *expect* and *assert*.
|
@ -1,62 +0,0 @@
|
||||
# Testing Email
|
||||
|
||||
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:
|
||||
|
||||
:::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.
|
||||
|
||||
:::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:
|
||||
|
||||
* 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.
|
||||
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 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 bounce-handling or any other auxiliary services of email
|
||||
|
||||
## 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:
|
||||
|
||||
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.
|
||||
2. `Email::set_mailer()` can be called to load in a new mailer object.
|
||||
3. `SapphireTest::setUp()` method calls `Email::set_mailer(new TestMailer())` to replace the default mailer with a `TestMailer` object. This replacement mailer doesn't actually do anything when it is asked to send an email; it just
|
||||
records the details of the email in an internal field that can be searched with `TestMailer::findEmails()`.
|
||||
4. `SapphireTest::assertEmailSent()` calls `TestMailer::findEmails()` to see if a mail-send was requested by the
|
||||
application.
|
||||
|
@ -0,0 +1,82 @@
|
||||
title: How to write a SapphireTest
|
||||
|
||||
# How to write a SapphireTest
|
||||
|
||||
Here is an example of a test which extends [api:SapphireTest] to test the URL generation of the page. It also showcases
|
||||
how you can load default records into the test database.
|
||||
|
||||
**mysite/tests/PageTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class PageTest extends SapphireTest {
|
||||
|
||||
/**
|
||||
* Defines the fixture file to use for this test class
|
||||
*
|
||||
/
|
||||
protected static $fixture_file = 'SiteTreeTest.yml';
|
||||
|
||||
/**
|
||||
* Test generation of the URLSegment values.
|
||||
*
|
||||
* Makes sure to:
|
||||
* - Turn things into lowercase-hyphen-format
|
||||
* - Generates from Title by default, unless URLSegment is explicitly set
|
||||
* - Resolves duplicates by appending a number
|
||||
*/
|
||||
public function testURLGeneration() {
|
||||
$expectedURLs = array(
|
||||
'home' => 'home',
|
||||
'staff' => 'my-staff',
|
||||
'about' => 'about-us',
|
||||
'staffduplicate' => 'my-staff-2'
|
||||
);
|
||||
|
||||
foreach($expectedURLs as $fixture => $urlSegment) {
|
||||
$obj = $this->objFromFixture('Page', $fixture);
|
||||
|
||||
$this->assertEquals($urlSegment, $obj->URLSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Firstly we define a static `$fixture_file`, this should point to a file that represents the data we want to test,
|
||||
represented as a YAML [Fixture](../fixtures). When our test is run, the data from this file will be loaded into a test
|
||||
database and discarded at the end of the test.
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
The `fixture_file` property can be path to a file, or an array of strings pointing to many files. The path must be
|
||||
absolute from your website's root folder.
|
||||
</div>
|
||||
|
||||
The second part of our class is the `testURLGeneration` method. This method is our test. When the test is executed,
|
||||
methods prefixed with the word `test` will be run.
|
||||
|
||||
<div class="notice" markdown="1">
|
||||
The test database is rebuilt every time one of these methods is run.
|
||||
</div>
|
||||
|
||||
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
|
||||
but not saved in the database anywhere, `objFromFixture` looks the `[api:DataObject]` up in memory rather than using the
|
||||
database. This means that you can use it to test the functions responsible for looking up content in the database.
|
||||
|
||||
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 any one of these assertions fail, so will the test method.
|
||||
|
||||
<div class="info" markdown="1">
|
||||
For more information on PHPUnit's assertions see the [PHPUnit manual](http://www.phpunit.de/manual/current/en/api.html#api.assert).
|
||||
</div>
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Unit Testing](../unit_testing)
|
||||
* [Fixtures](../fixtures)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:SapphireTest]
|
||||
* [api:FunctionalTest]
|
@ -0,0 +1,56 @@
|
||||
title: How to write a FunctionalTest
|
||||
|
||||
# How to Write a FunctionalTest
|
||||
|
||||
[api:FunctionalTest] test your applications `Controller` instances and anything else which requires a web request. The
|
||||
core of these tests are the same as `SapphireTest` unit tests but add several methods for creating [api:SS_HTTPRequest]
|
||||
and receiving [api:SS_HTTPResponse] objects. In this How To, we'll see how to write a test to query a page, check the
|
||||
response and modify the session within a test.
|
||||
|
||||
**mysite/tests/HomePageTest.php**
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class HomePageTest extends SapphireTest {
|
||||
|
||||
/**
|
||||
* Test generation of the view
|
||||
*/
|
||||
public function testViewHomePage() {
|
||||
$page = $this->get('home/');
|
||||
|
||||
// Home page should load..
|
||||
$this->assertEquals(200, $page->getStatusCode());
|
||||
|
||||
// We should see a login form
|
||||
$login = $this->submitForm("#LoginForm", null, array(
|
||||
'Email' => 'test@test.com',
|
||||
'Password' => 'wrongpassword'
|
||||
));
|
||||
|
||||
// wrong details, should now see an error message
|
||||
$this->assertExactHTMLMatchBySelector("#LoginForm p.error", array(
|
||||
"That email address is invalid."
|
||||
));
|
||||
|
||||
// If we login as a user we should see a welcome message
|
||||
$me = Member::get()->first();
|
||||
|
||||
$this->logInAs($me);
|
||||
$page = $this->get('home/');
|
||||
|
||||
$this->assertExactHTMLMatchBySelector("#Welcome", array(
|
||||
'Welcome Back'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Functional Testing](../functional_testing)
|
||||
* [Unit Testing](../unit_testing)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:FunctionalTest]
|
@ -0,0 +1,50 @@
|
||||
title: How to use a FixtureFactory
|
||||
|
||||
# How to use a FixtureFactory
|
||||
|
||||
The [api:FixtureFactory] is used to manually create data structures for use with tests. For more information on fixtures
|
||||
see the [Fixtures](../fixtures) documentation.
|
||||
|
||||
In this how to we'll use a `FixtureFactory` and a custom blue print for giving us a shortcut for creating new objects
|
||||
with information that we need.
|
||||
|
||||
:::php
|
||||
class MyObjectTest extends SapphireTest {
|
||||
|
||||
protected $factory;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function testSomething() {
|
||||
$MyObjectObj = $this->factory->createObject(
|
||||
'MyObject',
|
||||
array('MyOtherProperty' => 'My Custom Value')
|
||||
);
|
||||
|
||||
echo $MyObjectObj->MyProperty;
|
||||
// returns "My Default Value"
|
||||
|
||||
echo $myPageObj->MyOtherProperty;
|
||||
// returns "My Custom Value"
|
||||
}
|
||||
}
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Fixtures](../fixtures)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:FixtureFactory]
|
||||
* [api:FixtureBlueprint]
|
@ -0,0 +1,40 @@
|
||||
title: How to test emails within unit tests
|
||||
|
||||
# Testing Email within Unit Tests
|
||||
|
||||
SilverStripe's test system has built-in support for testing emails sent using the `[api:Email]` class. 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
|
||||
public function MyMethod() {
|
||||
$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();
|
||||
}
|
||||
|
||||
To test that `MyMethod` sends the correct email, use the [api:Email::assertEmailSent] method.
|
||||
|
||||
:::php
|
||||
$this->assertEmailSend($to, $from, $subject, $body);
|
||||
|
||||
// to assert that the email is sent to the correct person
|
||||
$this->assertEmailSent("someone@example.com", null, "/th.*e$/");
|
||||
|
||||
|
||||
Each of the arguments (`$to`, `$from`, `$subject` and `$body`) can be either one of the following.
|
||||
|
||||
* A string: match exactly that string
|
||||
* `null/false`: match anything
|
||||
* A PERL regular expression (starting with '/')
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Email](../../email)
|
||||
|
||||
## API Documentation
|
||||
|
||||
* [api:Email]
|
||||
|
Loading…
Reference in New Issue
Block a user