Review and rewrites for Testing Developer Guide

This commit is contained in:
Will Rossiter 2014-10-17 17:53:52 +13:00 committed by Cam Findlay
parent 4fcbc9a402
commit 576f34a539
17 changed files with 723 additions and 661 deletions

View File

@ -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:

View File

@ -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]

View 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]

View File

@ -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)

View 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">
`&amp;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">
`&amp;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]

View File

@ -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>

View File

@ -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/).

View File

@ -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.

View File

@ -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.

View File

@ -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
}
}

View File

@ -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`.

View File

@ -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*.

View File

@ -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.

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]