diff --git a/docs/en/02_Developer_Guides/00_Model/01_Page_Types.md b/docs/en/02_Developer_Guides/00_Model/01_Page_Types.md index 2997e0a0c..ed391f5f1 100644 --- a/docs/en/02_Developer_Guides/00_Model/01_Page_Types.md +++ b/docs/en/02_Developer_Guides/00_Model/01_Page_Types.md @@ -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: diff --git a/docs/en/02_Developer_Guides/05_Extending/index.md b/docs/en/02_Developer_Guides/05_Extending/index.md index 0d30602c5..74bfc712c 100644 --- a/docs/en/02_Developer_Guides/05_Extending/index.md +++ b/docs/en/02_Developer_Guides/05_Extending/index.md @@ -5,4 +5,4 @@ summary: Understand the ways to modify the built-in functionality through Extens ## How-to -[CHILDREN How_To] \ No newline at end of file +[CHILDREN Folder=How_To] \ No newline at end of file diff --git a/docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md b/docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md new file mode 100644 index 000000000..1771e640b --- /dev/null +++ b/docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md @@ -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 + assertEquals(2, Page::MyMethod()); + } + } + +
+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). +
+ +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. + +
+`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. +
+ +## 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) + +
+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`) +
+ +
+All command-line arguments are documented on [phpunit.de](http://www.phpunit.de/manual/current/en/textui.html). +
+ +### 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. + +
+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. +
+ +
+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` +
+ +## 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 + + + mysite/tests + cms/tests + framework/tests + + + + + + + + + sanitychecks + + + + +
+This configuration file doesn't apply for running tests through the "sake" wrapper +
+ + +### 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 + "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 + +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. + + +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 + + + framework/dev/ + framework/thirdparty/ + cms/thirdparty/ + + + mysite/thirdparty/ + + + +## 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] \ No newline at end of file diff --git a/docs/en/02_Developer_Guides/06_Testing/00_Why_Should_I_Test.md b/docs/en/02_Developer_Guides/06_Testing/00_Why_Should_I_Test.md deleted file mode 100644 index 3cb5a63fc..000000000 --- a/docs/en/02_Developer_Guides/06_Testing/00_Why_Should_I_Test.md +++ /dev/null @@ -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) - - diff --git a/docs/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md b/docs/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md new file mode 100644 index 000000000..b2320ab17 --- /dev/null +++ b/docs/en/02_Developer_Guides/06_Testing/01_Functional_Testing.md @@ -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. + +
+` ` characters are stripped from the content; make sure that your assertions take this into account. +
+ +### 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. + +
+` ` characters are stripped from the content; make sure that your assertions take this into account. +
+ +## Related Documentation + +* [How to write a FunctionalTest](how_tos/write_a_functionaltest) + +## API Documentation + +* [api:FunctionalTest] diff --git a/docs/en/02_Developer_Guides/06_Testing/01_PHPUnit_Configuration.md b/docs/en/02_Developer_Guides/06_Testing/01_PHPUnit_Configuration.md deleted file mode 100644 index 92db5bfbe..000000000 --- a/docs/en/02_Developer_Guides/06_Testing/01_PHPUnit_Configuration.md +++ /dev/null @@ -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: - - - - framework/dev/ - framework/thirdparty/ - cms/thirdparty/ - - - mysite/thirdparty/ - - - -## 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`: - - - - - mysite/tests/unit - othermodule/tests/unit - - - - - - -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 `` flag in your `phpunit.xml` (see [Appenix C: "Setting PHP INI settings"](http://www.phpunit.de/manual/current/en/appendixes.configuration.html)): - - - - - - - - -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: - - - - - - - - -
-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. -
diff --git a/docs/en/02_Developer_Guides/06_Testing/02_Behavior_Testing.md b/docs/en/02_Developer_Guides/06_Testing/02_Behavior_Testing.md new file mode 100644 index 000000000..7d4686b0f --- /dev/null +++ b/docs/en/02_Developer_Guides/06_Testing/02_Behavior_Testing.md @@ -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/). diff --git a/docs/en/02_Developer_Guides/06_Testing/02_Creating_a_Unit_Test.md b/docs/en/02_Developer_Guides/06_Testing/02_Creating_a_Unit_Test.md deleted file mode 100644 index 7d2126894..000000000 --- a/docs/en/02_Developer_Guides/06_Testing/02_Creating_a_Unit_Test.md +++ /dev/null @@ -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 - '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. diff --git a/docs/en/02_Developer_Guides/06_Testing/03_Creating_a_Functional_Test.md b/docs/en/02_Developer_Guides/06_Testing/03_Creating_a_Functional_Test.md deleted file mode 100644 index 23e963264..000000000 --- a/docs/en/02_Developer_Guides/06_Testing/03_Creating_a_Functional_Test.md +++ /dev/null @@ -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(), ' 1), $this->adminLoggedInSession()); - - $this->assertTrue(strpos($response3->getBody(), 'id="Root_Configuration"') !== false); - $this->assertTrue(strpos($response3->getBody(), 'assertTrue(strpos($response3->getBody(), '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. +
+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`. +
Note that we use the name of the relationship (Team), and not the name of the database field (TeamID).
-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); + +
+As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value logic will be executed as part of the +test. +
### 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.
-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.
-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 +
+It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally +mapped to their database identifiers. +
-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 - } - } diff --git a/docs/en/02_Developer_Guides/06_Testing/05_Testing_Guide_Troubleshooting.md b/docs/en/02_Developer_Guides/06_Testing/05_Testing_Guide_Troubleshooting.md deleted file mode 100644 index 0eec4bf8b..000000000 --- a/docs/en/02_Developer_Guides/06_Testing/05_Testing_Guide_Troubleshooting.md +++ /dev/null @@ -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.
-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`. diff --git a/docs/en/02_Developer_Guides/06_Testing/06_Glossary.md b/docs/en/02_Developer_Guides/06_Testing/06_Glossary.md deleted file mode 100644 index 276d8651d..000000000 --- a/docs/en/02_Developer_Guides/06_Testing/06_Glossary.md +++ /dev/null @@ -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*. diff --git a/docs/en/02_Developer_Guides/06_Testing/How_To/Testing_Email.md b/docs/en/02_Developer_Guides/06_Testing/How_To/Testing_Email.md deleted file mode 100644 index 9097aa7f5..000000000 --- a/docs/en/02_Developer_Guides/06_Testing/How_To/Testing_Email.md +++ /dev/null @@ -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. - diff --git a/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md b/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md new file mode 100644 index 000000000..ed8d69764 --- /dev/null +++ b/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md @@ -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 + '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. + +
+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. +
+ +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. + +
+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). +
+ +## Related Documentation + +* [Unit Testing](../unit_testing) +* [Fixtures](../fixtures) + +## API Documentation + +* [api:SapphireTest] +* [api:FunctionalTest] \ No newline at end of file diff --git a/docs/en/02_Developer_Guides/06_Testing/How_Tos/01_Write_a_FunctionalTest.md b/docs/en/02_Developer_Guides/06_Testing/How_Tos/01_Write_a_FunctionalTest.md new file mode 100644 index 000000000..de7297bb8 --- /dev/null +++ b/docs/en/02_Developer_Guides/06_Testing/How_Tos/01_Write_a_FunctionalTest.md @@ -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 + 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] diff --git a/docs/en/02_Developer_Guides/06_Testing/How_Tos/02_FixtureFactories.md b/docs/en/02_Developer_Guides/06_Testing/How_Tos/02_FixtureFactories.md new file mode 100644 index 000000000..47498b287 --- /dev/null +++ b/docs/en/02_Developer_Guides/06_Testing/How_Tos/02_FixtureFactories.md @@ -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] \ No newline at end of file diff --git a/docs/en/02_Developer_Guides/06_Testing/How_Tos/03_Testing_Email.md b/docs/en/02_Developer_Guides/06_Testing/How_Tos/03_Testing_Email.md new file mode 100644 index 000000000..8b456bff1 --- /dev/null +++ b/docs/en/02_Developer_Guides/06_Testing/How_Tos/03_Testing_Email.md @@ -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] +