silverstripe-framework/docs/en/topics/testing/create-sapphire-test.md

6.3 KiB

How To Create a Sapphire Test

A unit test class will test the behaviour of one of your [api:DataObjects]. This simple fragment of [api:SiteTreeTest] provides us the basics of creating unit tests.

:::php
<?php

/**

 * Tests for SiteTree
 */
class SiteTreeTest extends SapphireTest {
	/**

	 * Define the fixture file to use for this test class
	 */
	static $fixture_file = 'sapphire/tests/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',
			'product1' => '1-1-test-product',
			'product2' => 'another-product',
			'product3' => 'another-product-2',
			'product4' => 'another-product-3',
		);
		
		foreach($expectedURLs as $fixture => $urlSegment) {
			$obj = $this->objFromFixture('Page', $fixture);
			$this->assertEquals($urlSegment, $obj->URLSegment);
		}
	}
}

There are a number of points to note in this code fragment:

  • Your test is a subclass of SapphireTest. Both unit tests and functional tests are a subclass of [api:SapphireTest].
  • static $fixture_file is defined. The testing framework will automatically set up a new database for each of your tests. The initial database content will be sourced from the YML file that you list in $fixture_file. You must define this value. Note also that, for the time being, you can only point to one YML file for each test class.
  • Each method that starts with the word "test" will be executed by the TestRunner. Define as many as you like; the database will be rebuilt for each of these.
  • $this->objFromFixture($className, $identifier) can be used to select one of the objects named in your fixture file. To identify to the object, we provide a class name and an identifier. The identifier is specified in the YML 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.
  • $this->assertEquals() is one of the many assert... functions that PHPUnit provides us. See below for more information.

Assertion commands

$this->assertEquals() is an example of an assertion function. These functions form the basis of our tests - a test fails if and only if one or more of the assertions fail.

There are many assertions available:

  • See the PHPUnit manual chapter 22 for a listing of all PHPUnit's built-in assertions.
  • $this->assertEmailSent($to, $from, $subject, $content): When an email is "sent" during a test run, it's not actually sent. Instead, it is logged in an internal register. You can use assertEmailSent() to verify that an email was sent. Each of the arguments can be a string, for an exact match, or, a preg_match() compatible regular expression, if it starts with "/".

The Database YAML file

The main feature of [api:SapphireTest] over the raw PHPUnit classes is that SapphireTest will prepare a temporary database for you. The content of that database is provided in a special YAML file. YAML is a simple markup languages that uses tabs and colons instead of the more verbose XML tags, and because of this much better for developers creating files by hand.

We will begin with a sample file and talk our way through it.

Page:
    home:
        Title: Home
    about:
        Title: About Us
    staff:
        Title: Staff
        URLSegment: my-staff
        Parent: =>Page.about
    staffduplicate:
        Title: Staff
        URLSegment: my-staff
        Parent: =>Page.about
    products:
        Title: Products
    product1:
        Title: 1.1 Test Product
    product2:
        Title: Another Product
    product3:
        Title: Another Product
    product4:
        Title: Another Product
    contact:
        Title: Contact Us
        
ErrorPage:
    404:
        Title: Page not Found
        ErrorCode: 404

The contents of the YAML file are broken into three levels.

  • Top level: class names - Page and ErrorPage. This is the name of the dataobject class that should be created. The fact that ErrorPage is actually a subclass is irrelevant to the system populating the database. It just instantiates the object you specify.
  • Second level: identifiers - home, about, staff, staffduplicate, etc. These are the identifiers that you pass as the second argument of SapphireTest::objFromFixture(). Each identifier you specify delimits a new database record. This means that every record needs to have an identifier, whether you use it or not.
  • Third level: fields - each field for the record is listed as a 3rd level entry. In most cases, the field's raw content is provided. However, if you want to define a relationship, you can do so using "=>".

There are a couple of lines like this:

Parent: =>Page.about

This will tell the system to set the ParentID database field to the ID of the Page object with the identifier "about". This can be used on any has-one or many-many relationship. Note that we use the name of the relationship (Parent), and not the name of the database field (ParentID)

On many-many relationships, you should specify a comma separated list of values.

MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3

An crucial thing to note is that the YAML file specifies DataObjects, not database records. The database is populated by instantiating DataObject objects, setting the fields listed, and calling write(). This means that any onBeforeWrite() or default value logic will be executed as part of the test. This forms the basis of our testURLGeneration() test above.

For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff. When the fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2.

Finally, be aware that requireDefaultRecords() is not called by the database populator - so you will need to specify standard pages such as 404 and home in your YAML file.