PageRenderTime 41ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/docs/en/02_Developer_Guides/06_Testing/04_Fixtures.md

http://github.com/silverstripe/sapphire
Markdown | 418 lines | 305 code | 113 blank | 0 comment | 0 complexity | 406e3032b75ad6666bacc993dbef6918 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. ---
  2. title: Fixtures
  3. summary: Populate test databases with fake seed data.
  4. ---
  5. # Fixtures
  6. To test functionality correctly, we must use consistent data. If we are testing our code with the same data each
  7. time, we can trust our tests to yield reliable results and to identify when the logic changes. Each test run in
  8. SilverStripe starts with a fresh database containing no records. `Fixtures` provide a way to describe the initial data
  9. to load into the database. The [SapphireTest](api:SilverStripe\Dev\SapphireTest) class takes care of populating a test database with data from
  10. fixtures - all we have to do is define them.
  11. To include your fixture file in your tests, you should define it as your `$fixture_file`:
  12. **app/tests/MyNewTest.php**
  13. ```php
  14. use SilverStripe\Dev\SapphireTest;
  15. class MyNewTest extends SapphireTest
  16. {
  17. protected static $fixture_file = 'fixtures.yml';
  18. }
  19. ```
  20. You can also use an array of fixture files, if you want to use parts of multiple other tests.
  21. If you are using [api:SilverStripe\Dev\TestOnly] dataobjects in your fixtures, you must
  22. declare these classes within the $extra_dataobjects variable.
  23. **app/tests/MyNewTest.php**
  24. ```php
  25. use SilverStripe\Dev\SapphireTest;
  26. class MyNewTest extends SapphireTest
  27. {
  28. protected static $fixture_file = [
  29. 'fixtures.yml',
  30. 'otherfixtures.yml'
  31. ];
  32. protected static $extra_dataobjects = [
  33. Player::class,
  34. Team::class,
  35. ];
  36. }
  37. ```
  38. Typically, you'd have a separate fixture file for each class you are testing - although overlap between tests is common.
  39. Fixtures are defined in `YAML`. `YAML` is a markup language which is deliberately simple and easy to read, so it is
  40. ideal for fixture generation. Say we have the following two DataObjects:
  41. ```php
  42. use SilverStripe\ORM\DataObject;
  43. use SilverStripe\Dev\TestOnly;
  44. class Player extends DataObject implements TestOnly
  45. {
  46. private static $db = [
  47. 'Name' => 'Varchar(255)'
  48. ];
  49. private static $has_one = [
  50. 'Team' => 'Team'
  51. ];
  52. }
  53. class Team extends DataObject implements TestOnly
  54. {
  55. private static $db = [
  56. 'Name' => 'Varchar(255)',
  57. 'Origin' => 'Varchar(255)'
  58. ];
  59. private static $has_many = [
  60. 'Players' => 'Player'
  61. ];
  62. }
  63. ```
  64. We can represent multiple instances of them in `YAML` as follows:
  65. **app/tests/fixtures.yml**
  66. ```yml
  67. Team:
  68. hurricanes:
  69. Name: The Hurricanes
  70. Origin: Wellington
  71. crusaders:
  72. Name: The Crusaders
  73. Origin: Canterbury
  74. Player:
  75. john:
  76. Name: John
  77. Team: =>Team.hurricanes
  78. joe:
  79. Name: Joe
  80. Team: =>Team.crusaders
  81. jack:
  82. Name: Jack
  83. Team: =>Team.crusaders
  84. ```
  85. This `YAML` is broken up into three levels, signified by the indentation of each line. In the first level of
  86. indentation, `Player` and `Team`, represent the class names of the objects we want to be created.
  87. The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are **identifiers**. Each identifier you specify
  88. represents a new object and can be referenced in the PHP using `objFromFixture`
  89. ```php
  90. $player = $this->objFromFixture('Player', 'jack');
  91. ```
  92. The third and final level represents each individual object's fields.
  93. A field can either be provided with raw data (such as the names for our Players), or we can define a relationship, as
  94. seen by the fields prefixed with `=>`.
  95. Each one of our Players has a relationship to a Team, this is shown with the `Team` field for each `Player` being set
  96. to `=>Team.` followed by a team name.
  97. [info]
  98. Take the player John in our example YAML, his team is the Hurricanes which is represented by `=>Team.hurricanes`. This
  99. sets the `has_one` relationship for John with with the `Team` object `hurricanes`.
  100. [/info]
  101. [hint]
  102. Note that we use the name of the relationship (Team), and not the name of the
  103. database field (TeamID).
  104. [/hint]
  105. [hint]
  106. Also be aware the target of a relationship must be defined before it is referenced, for example the `hurricanes` team must appear in the fixture file before the line `Team: =>Team.hurricanes`.
  107. [/hint]
  108. This style of relationship declaration can be used for any type of relationship (i.e `has_one`, `has_many`, `many_many`).
  109. We can also declare the relationships conversely. Another way we could write the previous example is:
  110. ```yml
  111. Player:
  112. john:
  113. Name: John
  114. joe:
  115. Name: Joe
  116. jack:
  117. Name: Jack
  118. Team:
  119. hurricanes:
  120. Name: Hurricanes
  121. Origin: Wellington
  122. Players: =>Player.john
  123. crusaders:
  124. Name: Crusaders
  125. Origin: Canterbury
  126. Players: =>Player.joe,=>Player.jack
  127. ```
  128. The database is populated by instantiating `DataObject` objects and setting the fields declared in the `YAML`, then
  129. calling `write()` on those objects. Take for instance the `hurricances` record in the `YAML`. It is equivalent to
  130. writing:
  131. ```php
  132. $team = new Team([
  133. 'Name' => 'Hurricanes',
  134. 'Origin' => 'Wellington'
  135. ]);
  136. $team->write();
  137. $team->Players()->add($john);
  138. ```
  139. [notice]
  140. As the YAML fixtures will call `write`, any `onBeforeWrite()` or default value logic will be executed as part of the
  141. test.
  142. [/notice]
  143. ### Fixtures for namespaced classes
  144. As of SilverStripe 4 you will need to use fully qualfied class names in your YAML fixture files. In the above examples, they belong to the global namespace so there is nothing requires, but if you have a deeper DataObject, or it has a relationship to models that are part of the framework for example, you will need to include their namespaces:
  145. ```yml
  146. MyProject\Model\Player:
  147. john:
  148. Name: join
  149. MyProject\Model\Team:
  150. crusaders:
  151. Name: Crusaders
  152. Origin: Canterbury
  153. Players: =>MyProject\Model\Player.john
  154. ```
  155. [notice]
  156. If your tests are failing and your database has table names that follow the fully qualified class names, you've probably forgotten to implement `private static $table_name = 'Player';` on your namespaced class. This property was introduced in SilverStripe 4 to reduce data migration work. See [DataObject](api:SilverStripe\ORM\DataObject) for an example.
  157. [/notice]
  158. ### Defining many_many_extraFields
  159. `many_many` relations can have additional database fields attached to the relationship. For example we may want to
  160. declare the role each player has in the team.
  161. ```php
  162. use SilverStripe\ORM\DataObject;
  163. class Player extends DataObject
  164. {
  165. private static $db = [
  166. 'Name' => 'Varchar(255)'
  167. ];
  168. private static $belongs_many_many = [
  169. 'Teams' => 'Team'
  170. ];
  171. }
  172. class Team extends DataObject
  173. {
  174. private static $db = [
  175. 'Name' => 'Varchar(255)'
  176. ];
  177. private static $many_many = [
  178. 'Players' => 'Player'
  179. ];
  180. private static $many_many_extraFields = [
  181. 'Players' => [
  182. 'Role' => "Varchar"
  183. ]
  184. ];
  185. }
  186. ```
  187. To provide the value for the `many_many_extraField` use the YAML list syntax.
  188. ```yml
  189. Player:
  190. john:
  191. Name: John
  192. joe:
  193. Name: Joe
  194. jack:
  195. Name: Jack
  196. Team:
  197. hurricanes:
  198. Name: The Hurricanes
  199. Players:
  200. - =>Player.john:
  201. Role: Captain
  202. crusaders:
  203. Name: The Crusaders
  204. Players:
  205. - =>Player.joe:
  206. Role: Captain
  207. - =>Player.jack:
  208. Role: Winger
  209. ```
  210. ## Fixture Factories
  211. While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention.
  212. Alternatively, you can use the [FixtureFactory](api:SilverStripe\Dev\FixtureFactory) class, which allows you to set default values, callbacks on object
  213. creation, and dynamic/lazy value setting.
  214. [hint]
  215. `SapphireTest` uses `FixtureFactory` under the hood when it is provided with YAML based fixtures.
  216. [/hint]
  217. The idea is that rather than instantiating objects directly, we'll have a factory class for them. This factory can have
  218. *blueprints* defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a
  219. name, which is usually set to the class it creates such as `Member` or `Page`.
  220. Blueprints are auto-created for all available DataObject subclasses, you only need to instantiate a factory to start
  221. using them.
  222. ```php
  223. use SilverStripe\Core\Injector\Injector;
  224. $factory = Injector::inst()->create('FixtureFactory');
  225. $obj = $factory->createObject('Team', 'hurricanes');
  226. ```
  227. In order to create an object with certain properties, just add a third argument:
  228. ```php
  229. $obj = $factory->createObject('Team', 'hurricanes', [
  230. 'Name' => 'My Value'
  231. ]);
  232. ```
  233. [warning]
  234. It is important to remember that fixtures are referenced by arbitrary identifiers ('hurricanes'). These are internally
  235. mapped to their database identifiers.
  236. [/warning]
  237. After we've created this object in the factory, `getId` is used to retrieve it by the identifier.
  238. ```php
  239. $databaseId = $factory->getId('Team', 'hurricanes');
  240. ```
  241. ### Default Properties
  242. Blueprints can be overwritten in order to customise their behavior. For example, if a Fixture does not provide a Team
  243. name, we can set the default to be `Unknown Team`.
  244. ```php
  245. $factory->define('Team', [
  246. 'Name' => 'Unknown Team'
  247. ]);
  248. ```
  249. ### Dependent Properties
  250. Values can be set on demand through anonymous functions, which can either generate random defaults, or create composite
  251. values based on other fixture data.
  252. ```php
  253. $factory->define('Member', [
  254. 'Email' => function($obj, $data, $fixtures) {
  255. if(isset($data['FirstName']) {
  256. $obj->Email = strtolower($data['FirstName']) . '@example.com';
  257. }
  258. },
  259. 'Score' => function($obj, $data, $fixtures) {
  260. $obj->Score = rand(0,10);
  261. }
  262. )];
  263. ```
  264. ### Relations
  265. Model relations can be expressed through the same notation as in the YAML fixture format described earlier, through the
  266. `=>` prefix on data values.
  267. ```php
  268. $obj = $factory->createObject('Team', 'hurricanes', [
  269. 'MyHasManyRelation' => '=>Player.john,=>Player.joe'
  270. ]);
  271. ```
  272. #### Callbacks
  273. Sometimes new model instances need to be modified in ways which can't be expressed in their properties, for example to
  274. publish a page, which requires a method call.
  275. ```php
  276. $blueprint = Injector::inst()->create('FixtureBlueprint', 'Member');
  277. $blueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
  278. $obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
  279. });
  280. $page = $factory->define('Page', $blueprint);
  281. ```
  282. Available callbacks:
  283. * `beforeCreate($identifier, $data, $fixtures)`
  284. * `afterCreate($obj, $identifier, $data, $fixtures)`
  285. ### Multiple Blueprints
  286. Data of the same type can have variations, for example forum members vs. CMS admins could both inherit from the `Member`
  287. class, but have completely different properties. This is where named blueprints come in. By default, blueprint names
  288. equal the class names they manage.
  289. ```php
  290. $memberBlueprint = Injector::inst()->create('FixtureBlueprint', 'Member', 'Member');
  291. $adminBlueprint = Injector::inst()->create('FixtureBlueprint', 'AdminMember', 'Member');
  292. $adminBlueprint->addCallback('afterCreate', function($obj, $identifier, $data, $fixtures) {
  293. if(isset($fixtures['Group']['admin'])) {
  294. $adminGroup = Group::get()->byId($fixtures['Group']['admin']);
  295. $obj->Groups()->add($adminGroup);
  296. }
  297. });
  298. $member = $factory->createObject('Member'); // not in admin group
  299. $admin = $factory->createObject('AdminMember'); // in admin group
  300. ```
  301. ## Related Documentation
  302. * [How to use a FixtureFactory](how_tos/fixturefactories/)
  303. ## API Documentation
  304. * [FixtureFactory](api:SilverStripe\Dev\FixtureFactory)
  305. * [FixtureBlueprint](api:SilverStripe\Dev\FixtureBlueprint)