Laravel Testing Decoded
Laravel Testing Decoded
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. 2013 JeffreyWay
Contents
Welcome . . . . . . . . . . . . It Has Begun . . . . . . . . . Is This Book For Me? . . . . Why Laravel-Specific? . . . Exercises . . . . . . . . . . . Errata . . . . . . . . . . . . How to Consume This Book Get in Touch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 2 3 3 3 3 5 6 6 7 7 7 8 9 9 9 10 10 11 12 13 13 14 17 17 18 18 18 19 19
Into the Great Wide Open . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Chapter 1: Test All The Things . . . . . . . . . . . . . . . . . . . . . You Already Test . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Wins From TDD . . . . . . . . . . . . . . . . . . . . . . . . . . . 1. Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. Contribution . . . . . . . . . . . . . . . . . . . . . . . . . . 3. Big-Boy Pants . . . . . . . . . . . . . . . . . . . . . . . . . 4. Testability Improves Architecture . . . . . . . . . . . . . . 5. Documentation . . . . . . . . . . . . . . . . . . . . . . . . 6. Its Fun . . . . . . . . . . . . . . . . . . . . . . . . . . . . . What Should I Test? . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Signs of Untestable Code . . . . . . . . . . . . . . . . . . . . . . 1. New Operators . . . . . . . . . . . . . . . . . . . . . . . . 2. Control-Freak Constructors . . . . . . . . . . . . . . . . . . 3. And, And, And . . . . . . . . . . . . . . . . . . . . . . . . 4 Ways to Spot a Class With Too Many Responsibilities 4. Too Many Paths? Polymorphism to the Rescue! . . . . . . . 5. Too Many Dependencies . . . . . . . . . . . . . . . . . . . 6. Too Many Bugs . . . . . . . . . . . . . . . . . . . . . . . . Test Jargon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unit Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . Model Testing . . . . . . . . . . . . . . . . . . . . . . . . . . Integration Testing . . . . . . . . . . . . . . . . . . . . . . . . Functional (Controller) Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
CONTENTS
Acceptance Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Relax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Chapter 2: Introducing PHPUnit . . . . . . Installation . . . . . . . . . . . . . . . . . Making Packages Available Globally Assertions 101 . . . . . . . . . . . . . . . Decoding A Test Class Structure . . assertTrue . . . . . . . . . . . . . . assertEquals . . . . . . . . . . . . . assertSame . . . . . . . . . . . . . . assertContains . . . . . . . . . . . . assertArrayHasKey . . . . . . . . . assertInternalType . . . . . . . . . . assertInstanceOf . . . . . . . . . . . Asserting Exceptions . . . . . . . . . Summary . . . . . . . . . . . . . . . . . Chapter 3: Configuring PHPUnit Options . . . . . . . . . . . . . Technicolor . . . . . . . . . Bootstrapping . . . . . . . Output Formats . . . . . . XML Configuration File . . . . Continuous Testing . . . . . . . Watching Files . . . . . . . Triggering Multiple Files . . Some Vim-Specific Advice Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19 21 22 22 24 25 27 27 28 29 30 31 31 32 33 33 35 35 35 37 38 38 40 43 44 45 46 47 47 48 50 50 51 51 52 53 53 53 54 58
Chapter 4: Making PHPUnit Less Verbose . . . . . . . . . . . . . . . . . . . . . . . . . . . Importing Assertions as Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Applying the Laravel Style to PHPUnit . . . . . . . . . . . . . . . . . . . . . . . . . . . . Chapter 5: Unit Testing 101 . . . . . . . My Struggles . . . . . . . . . . . . . Unit Testing . . . . . . . . . . . . . . Arrange, Act, Assert . . . . . . . . . Testing in Isolation . . . . . . . . . . Tests Should Not Be Order-Dependent Test-Driven Development . . . . . . . Behavior-Driven Development . . . . Testing Functions . . . . . . . . . . . Slime vs. Generalize . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
CONTENTS
Slime . . . . . . . . . . . Generalize . . . . . . . . Making the Test Pass . . . . . . . Testing Classes . . . . . . . . . . . . Refactoring the Tests . . . . . . . Refactoring the Production Code Polymorphism . . . . . . . . . . Extensibility . . . . . . . Mocks . . . . . . . . . . . Project Complete . . . . . . . . . . . Final Source . . . . . . . . . . . Summary . . . . . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
58 58 59 63 69 71 72 75 75 77 77 81 82 82 84 84 85 86 86 87 90 91 92 92 92 93 93 94 95 97 102 104 106 106 107 107 108 108 108 109
Chapter 6: Contributing to Laravel Using TDD Creating a Proposal . . . . . . . . . . . . . . . Your Local Copy . . . . . . . . . . . . . . . . . Branching . . . . . . . . . . . . . . . . . . . . Coding Guidelines . . . . . . . . . . . . . . . Hands On . . . . . . . . . . . . . . . . . . . . Agenda . . . . . . . . . . . . . . . . . . . selectYear . . . . . . . . . . . . . . . . . . selectMonth . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . Chapter 7: Testing Models . . . . . . . . . . What to Test . . . . . . . . . . . . . . . . . Accessors and Mutators . . . . . . . . . . . Cat Years Example . . . . . . . . . . . Password Hashing Example . . . . . . Custom Methods . . . . . . . . . . . . . . Simple Query Methods . . . . . . . . . . . Validations . . . . . . . . . . . . . . . . . . Helpers . . . . . . . . . . . . . . . . . Factories . . . . . . . . . . . . . . . . . . . Laravel Test Helpers . . . . . . . . . . . . . Factories . . . . . . . . . . . . . . . . Overrides . . . . . . . . . . . . . . . . Models . . . . . . . . . . . . . . . . . Test Helpers . . . . . . . . . . . . . . assertValid and assertNotValid . Asserting Relationships . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
CONTENTS
Chapter 8: Easier Testing With Mockery . . Mocking Decoded . . . . . . . . . . . . . . Installation . . . . . . . . . . . . . . . . . . The Dilemma . . . . . . . . . . . . . . . . Dependency Injection . . . . . . . . . The Solution . . . . . . . . . . . . . . . . . Simple Mock Objects . . . . . . . . . Return Values From Mocked Methods Expectations . . . . . . . . . . . . . . . . . Partial Mocks . . . . . . . . . . . . . . . . Hamcrest . . . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . Chapter 9: Test Databases . . Test Databases . . . . . . . . Specifying the Environment Calling Artisan From Tests . Try It Out . . . . . . . . . . Databases in Memory . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
110 110 112 113 114 117 119 120 122 123 125 127 128 128 129 130 131 132 133 134 134 135 138 139 140 140 141 141 142 143 143 146 146 150 150 151 154 158 161 164
Chapter 10: Just Swap That Thang . Mockery . . . . . . . . . . . . . . Testing . . . . . . . . . . . . . . . Mocking Events . . . . . . . Summary . . . . . . . . . . . . .
Chapter 11: Testing Controllers . . . . What Does a Controller Do? . . . . . 3 Steps to Testing Controllers . . . . . The Hello World of Controller Testing Overloading is Your Friend . . . Calling Controller Actions . . . . Laravels Helper Assertions . . . . . . Mocking the Database . . . . . . . . Required Refactoring . . . . . . The IoC Container . . . . . . . . Redirections . . . . . . . . . . . . . . Paths . . . . . . . . . . . . . . . . . . Repositories . . . . . . . . . . . . . . Structure . . . . . . . . . . . . . . . . Updating the Tests . . . . . . . . Crawling the DOM . . . . . . . . . .
CONTENTS
Ensure View Contains Text Basic Traversing . . . . . . Fetch By Position . . Fetch First or Last . Fetch Siblings . . . . Fetch Children . . . Capture Text Content . . . Forms . . . . . . . . . . . . . . Summary . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
165 165 165 165 166 166 166 167 168 169 169 169 170 171 172 172 174 176 177 179 180 181 181 181 183 186 188 188 188 190 191 193 195 195 196 197 198 202 207
Chapter 12: The IoC Container . . . . . . . . . Dependency Injection? . . . . . . . . . . . . Constructor Injection . . . . . . . . . . Setter Injection . . . . . . . . . . . . . . Resolving . . . . . . . . . . . . . . . . . . . Solution 1: Defaults . . . . . . . . . . . Solution 2: Resolving . . . . . . . . . . . App Bindings . . . . . . . . . . . . . . . . . Interfaces . . . . . . . . . . . . . Example 2: Automatic Resolution Extra Credit . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . .
Chapter 13: Test-Driving Artisan Commands Exercise Commands 101 . . . . . . . . . . . . . . . . . . . . . Scaffolding . . . . . . . . . . . . . . . . . . . . . Arguments . . . . . . . . . . . . . . . . . . . . . Options . . . . . . . . . . . . . . . . . . . . . . . Single Responsibility Principle . . . . . . . . . . Exercise . . . . . . . . . . . . . . . . . . . . . . . . . Create the Package . . . . . . . . . . . . . . . . . Generating the Command . . . . . . . . . . . . . Service Providers . . . . . . . . . . . . . . . . . . Testing Artisan Commands . . . . . . . . . . . . Planning . . . . . . . . . . . . . . . . . . Expectations . . . . . . . . . . . . . . . . Dependency Injection . . . . . . . . . . . Generator Class . . . . . . . . . . . . . . . Making the Test Pass . . . . . . . . . . . . Testing the Model Generator . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . .
CONTENTS
APIs in Laravel . . . . . . . . . . . . . . . . . . . . . Three Components to Writing an API in Laravel 1. Authentication . . . . . . . . . . . . . . 2. Route Prefixing . . . . . . . . . . . . . . 3. Return JSON . . . . . . . . . . . . . . . Testing APIs . . . . . . . . . . . . . . . . . . . . . . . Use a Database in Memory . . . . . . . . . . . . Migrate the Database for Each Test . . . . . . . . Enable Filters . . . . . . . . . . . . . . . . . . . . Set the Authenticated User . . . . . . . . . . . . Test Examples . . . . . . . . . . . . . . . . . . . . . . User Must Be Authenticated . . . . . . . . . . . . Check For Error . . . . . . . . . . . . . . . . . . Fetch All Photos For the Authenticated User . . . Refactoring . . . . . . . . . . . . . . . . . Updating a Photo . . . . . . . . . . . . . . . . . Factories . . . . . . . . . . . . . . . . . . Specifying Options . . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . Chapter 15: Acceptance Testing With Codeception An Amuse Bouche . . . . . . . . . . . . . . . . . Testing Refresher . . . . . . . . . . . . . . . . . . Acceptance Testing . . . . . . . . . . . . . . Installation . . . . . . . . . . . . . . . . . . . . . . Global Installation . . . . . . . . . . . . . . . Local Installation . . . . . . . . . . . . . . . . Bootstrapping . . . . . . . . . . . . . . . . . . . . Configuring Acceptance Tests . . . . . . . . . . . Generate a Test . . . . . . . . . . . . . . . . . . . Manual Approach . . . . . . . . . . . . . . . Generator Approach . . . . . . . . . . . . . . Decoding the Command . . . . . . . . Writing the First Test . . . . . . . . . . . . . Running All Tests . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
208 208 208 210 211 213 213 214 215 215 215 215 216 217 217 218 220 221 222 223 224 224 225 226 227 228 229 230 230 231 231 232 233 233 235 236 236 236 238 239 240
Chapter 16: Authentication With Codeception Exercise The Feature . . . . . . . . . . . . . . . . . . . . . . . . Translating the Feature for Codeception . . . . . . . . . Register Routes . . . . . . . . . . . . . . . . . . . . . . Building the Form . . . . . . . . . . . . . . . . . . Resources . . . . . . . . . . . . . . . . . . . . . . .
CONTENTS
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
240 241 243 245 246 246 246 247 247 251 252 253 253 254 255 258 259 259 259 260 260 260 260 260 261 261 261 262 264 264 264 264 265 266 266 267
Chapter 17: Functional Testing in Codeception The Laravel4 Module . . . . . . . . . . . . . . The DB Module . . . . . . . . . . . . . . Updating TestGuy . . . . . . . . . . . . . . . . Registering a User . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . .
Chapter 18: Continuous Integration With Travis CI . Hello, Travis . . . . . . . . . . . . . . . . . . . . . . 1. Connect . . . . . . . . . . . . . . . . . . . . 2. Register Hooks . . . . . . . . . . . . . . . . . 3. Configure . . . . . . . . . . . . . . . . . . . Build Configuration . . . . . . . . . . . . . . . . . . Absolute Basics . . . . . . . . . . . . . . . . . Bootstrapping . . . . . . . . . . . . . . . . . . Register Dependencies . . . . . . . . . . . . . . Notifications . . . . . . . . . . . . . . . . . . . Turn Off Notifications . . . . . . . . . . Set Recipients . . . . . . . . . . . . . . . IRC . . . . . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . .
Frequently Asked Questions (A Living Document) . . . . . . 1. How Do I Test Global Functions? . . . . . . . . . . . . . . Mock It . . . . . . . . . . . . . . . . . . . . . . . . . . . The Namespacing Trick . . . . . . . . . . . . . . . . . . 2. How Do I Create a SQL Dump for Codeception Tests? . . . 3. How Do I Test Protected Methods? . . . . . . . . . . . . . 4. Should I Test Getters and Setters? . . . . . . . . . . . . . . 5. Why Is PHPUnit Ignoring My Filters? . . . . . . . . . . . . 6. Can I Mock a Method in the Class Im Testing? . . . . . . . Traditional Partial Mocks . . . . . . . . . . . . . . . . . Passive Partial Mocks . . . . . . . . . . . . . . . . . . . 7. I Prefer Using Underscores For Test Names. Is That Okay? .
Welcome
Ive seen it way too many times. As your application grows, so does your sloppy, untested codebase. Before long, you begin to drown, as your ability to manually test the application becomes unrealistic, or even impossible! Its at these specific times, when you begin to realize the down-right necessity for testing. Sure, you might have read a TDD book in the past, but, like many things in life, we require real-life experience, before we suddenly - in that wonderful aha moment - get it. The only problem is that testing can be a tricky thing. In fact, its quite possible that your codebase, as it currently stands, is untestable! What you may not realize is that, while, yes, testing does help to ensure that your code works as expected, following this pattern will also make you a better developer. That messy, untestable spaghetti code that you might have snuck into your project in the past will never happen again. Trust me: as soon as you bring the question how could I test this to the forefront of every new piece of code, youll, with a smile on your face, look back to your former self, and laugh at your crazy, cowboy ways. Welcome to modern software development.
While the principles of testing (and TDD) are language-agnostic, when it comes to execution, there are a variety of tools and techniques at your finger tips. This book is as much an introduction to TDD, as it is a deep analysis of the Laravel way of testing applications.
It Has Begun
When it comes to programming languages, everyone has an opinion. And when the topic of conversation switches to PHP specifically, well, prepare yourself for the vitriol. Despite the fact that the language has matured significantly in the last five years, there are those who, like a father staring at his grown-up daughter, still cant help but see PHP as a baby. The naysayers dont see version 5.5, or OOP, or modern frameworks like Laravel, or Composer, or a growing emphasis on test-first development. No, what they see is that old sloppy PHP 4 code, and, worse, poorly made WordPress themes from 2008. Is PHP as beautiful a language as Ruby? No. Is its API inconsistent from time to time? Definitely. Has its community lead the development world, in terms of innovation and software design? Certainly not. So the question is, why? Why does PHP dominate - to the point of 80% market share - when competing languages are admittedly more elegant? Well, maybe theres something else to its success. 1
Welcome
Maybe, the fact that you can create a file, echo hello world, and immediately see the output in a browser is far more powerful and user friendly than we give it credit for. Maybe its flexibility is a virtue, rather than a vice. Since when did ease-of-use become something that others mocked? Or, perhaps the simple truth is that PHP is not the new hotness. Its not overly sexy. Its not in beta. But, you know what? We get stuff done. Say as much as you want about PHP 4. While youre doing that, the rest of us will be building things with the latest that the language and surrounding ecosystem have to offer. The time for hating PHP is over. The PHP renaissance has begun. We use modern object-oriented techniques, we share packages through Composer, we embrace version control and continuous integration, we evangelize modern frameworks, we believe in testing (you soon will too), we welcome newcomers (rather than lock the door), and we do it all with a smile. The best part is that, as a Laravel user, youre at the forefront of this new modern movement! When I first joined Laravels IRC channel, within minutes, somebody said Welcome to the family. Nothing describes our community more beautifully than that. Were all in this together. This is the PHP community I love. If you purchased this book, it sounds like you could use a bit of help in the testing department. In Laravel spirit, welcome to the family. Lets figure this out together.
Why Laravel-Specific?
Sure, many of the techniques outlined in this book can be applied to any language or framework, however, in my experiences, its best to take your first steps into this new world in as comfortable shoes as possible. Can you learn test-driven development from a Java book? Absolutely! Would it be easier through the lens of the language and framework that you already know? Certainly.
Welcome
Secondly, the downside to a generic testing book is that I wouldnt be able to demonstrate many of the PHP-specific features and packages that I use in my every-day coding. This includes everything from PHPUnit helper packages, to acceptance testing frameworks, like Codeception. Finally, its my hope that, as you read through this book, in addition to improving your testing knowledge, youll also pick up a variety of Laravel-specific tips and tricks.
Exercises
Sporadically throughout this book, Exercise chapters will be provided. Think of these as highly in depth tutorials that you are encouraged to work through, as you read the chapter. Theory can only take us so far; its the actual coding that ultimately commits these patterns and techniques to memory. So, when you come to an Exercise chapter, pull out the computer and join me through each step!
Errata
Please note that, while Ive made every attempt to ensure that this book is free of errors and typos, my human-ness virtually guarantees that some will sneak in! If you notice any mistakes, please file an issue on GitHub, and Ill update the book as soon as possible. As a reward, a five minute hug will be granted to each bug filer.
Get in Touch
It sounds like were going to be spending a lot of time together, as you work your way through this book. If youd like to attach a face to the author, and, perhaps, ask some questions along the way, be sure to say hello. IRC (#laravel channel): JeffreyWay
https://github.com/JeffreyWay/Laravel-Testing-Decoded
Welcome
Twitter: @jeffrey_way
http://twitter.com/jeffrey_way
When developers first discover the wonders of test-driven development, its like gaining entrance to a new and better world with less stress and insecurity. - DHH
1. Security
Should you accidentally make a mistake or break a piece of existing functionality, the test robots will notify you right away. Imagine making an edit, clicking save, and immediately receiving feedback on whether you screwed up. How much better would you sleep at night? Remember that terribly coded class you were too afraid to refactor, because you might break the code? If tests were backing up that code, your fear would have been unwarranted.
2. Contribution
As you begin developing open source software, youll likely leverage the convenience and power of social coding through GitHub. Eventually (one of the perks), other members of the community will begin contributing to your projects when they encounter bugs or hope to implement new functionality. However, if your project doesnt contain a test suite, when developers submit pull requests, how could you (or they) possibly determine if their changes have broken the code? The answer? You cant - not without manually testing every possible path through the code. Who has the time to do that for every pull request? Think of a highly tested project as a well-oiled machine. If I want to contribute to your project, I only need to follow a handful of steps: 1. 2. 3. 4. 5. Clone the repository Write a test that describes the bug (testThrowsExceptionIfUserNameDoesNotExist) Make the necessary changes to fix it Run the tests to ensure that everything returns green (success) Commit the changes, and submit my pull request
There are even continuous integration services, like Travis, which will automatically trigger a projects tests when a pull request is submitted. If those tests fail, I immediately know that it shouldnt be merged without further tweaking.
https://travis-ci.org/
travis-ci.org
3. Big-Boy Pants
If I may go on a tangent for a moment, when it comes to the PHP community, my view is that WordPress has been a double-edged sword. On one hand, it brought blogging to the masses. This is undeniable, and must be respected. It also provided an easy-to-use theming framework for developers. Create an index.php file, insert a loop to fetch the recent posts, and then style. What could be easier than that? Well, thats true. However, it also inadvertently nurtured a community of PHP developers who hesitated to reach beyond WordPress for new projects. Consequently, modern practices and patterns, such as test-driven development, MVC, and version control, are largely foreign to them. This unfortunate truth has had two side-effects: 1. Much of the vitriol directed toward the PHP community is the result of PHP 4 and WordPress code. 2. Making the leap from WordPress to a full-stack framework, like Laravel, can prove incredibly difficult. Due to the number of new tools and patterns, the learning curve can be quite steep. Is WordPress responsible for these side-effects? Yes, and no. One things for sure, though: it certainly
hasnt pushed the boundaries of software craftsmanship. In fact, testing is ignored for 95% (made up number ) of the available WordPress plugins. Eventually, though, we all learn to put on our big-boy pants. We refer to that old-fashioned practice of coding without thinking as being a cowboy. Dont plan, dont think, dont test; just start coding, guns a blazing, while frantically refreshing the browser to determine if each change has broken the application. Were better than that. Were developers. Lets not be cowboys. An interesting transition takes place, when you force yourself to think before coding: it actually improves the quality of the code. Who would have thought? What youll soon learn is that theres more to testing than simply verifying that a method performs as expected. When we test, we interact with the class or API before it has been written. This forces us to remove all constraints and instead focus on readability. What would be the most readable way to fetch data from this web service? Write it as such, watch it fail, and then make it work. Its a beautiful thing!
5. Documentation
A huge, huge bonus to writing tests is that they provide free documentation for the system. Want to know what functionality a particular class offers? Poke around the tests, and, if they were named properly (meaning that they describe the behavior of the SUT, or system under test), then youll have a full understanding in no time!
6. Its Fun
Lets face it: were geeks. And what geek doesnt enjoy a good game? A fun side-effect to test-driven development is that it turns your job into a game. How can I take this code from red to green? Follow each step until you get there. It may sound silly at first, but I promise you: its fun. Youll see for yourself soon enough.
10
11
1. New Operators
The principles of unit testing dictate that we should test in isolation. Well cover this concept more in future chapters, but, in short, your goal should be to test the current class, and nothing else. Dont access the database, dont test that your Filesystem class fetches some data from a web service. Those should have their own tests, so dont double up. Once you begin littering the new operator throughout your classes, you break this rule. Remember: testing in isolation requires that the class, itself, does not instantiate other objects. Anti-pattern:
1 2 3 4 5 6 7
public function fetch($url) { // We can't test this! $file = new Filesystem; return $this->data = $file->get($url); }
This is one of those situations where PHP isnt quite as flexible as we might hope. While languages like Ruby offer the ability to re-open a class (known as monkey-patching) and override methods (particularly helpful for testing ), PHP, unfortunately, does not - at least, not without recompiling PHP with special extensions. As such, we must make use of dependency injection religiously. Better:
1 2 3 4 5 6 7 8 9 10 11
protected $file; public function __construct(Filesystem $file) { $this->file = $file; } public function fetch($url) { return $this->data = $this->file->get($url); }
With this modification, a mocked version of the Filesystem class can be injected, allowing for complete testability. Dont worry if the code below is foreign to you. Youll learn the inner workings soon! For now, simply try to soak it in.
12
public function testFetchesData() { $file = Mockery::mock('Filesystem'); $file->shouldReceive('get')->once()->andReturn('foo'); $someClass = new SomeClass($file); $data = $someClass->fetch('http://example.com'); $this->assertEquals('foo', $data); }
The only time when its acceptable to instantiate a class inside of another class is when that object is what we refer to as a value-object, or a simple container with getters and setters that doesnt do any real work.
Tip: Hunt down the new keyword in your classes like a hawk. Theyre code smells in PHP (at least for 90% of the cases)!
2. Control-Freak Constructors
A constructors only responsibility should be to assign dependencies. Think of this as your class asking for things. Can I have the Filesystem class, please? If youre doing anything beyond that, consider refactoring. Anti-pattern:
1 2 3 4 5 6 7 8
public function __construct(Filesystem $file, Cache $cache) { $this->file = $file; $this->cache = $cache; $data = $this->file->get('http://example.com'); $this->write($data); }
Better:
13
public function __construct(Filesystem $file, Cache $cache) { $this->file = $file; $this->cache = $cache; }
The reason why we do this is because, when testing, youll repeatedly follow the same process: 1. Arrange 2. Act 3. Assert If a class constructor is littered with its own actions and method calls, each test you write must account for these actions.
Tip: Keep it simple: limit your constructors to dependency assignments.
14
Here are some examples to get you started: A FileLogger class is responsible for logging data to a file. A TwitterStream class fetches and returns tweets from the Twitter API, when given a username. A Validator class is responsible for validating data against a set of rules. A SQLBuilder builds a SQL query, given a set of data. A UserAuthenticator class determines if the provided login credentials are correct. Notice how, in none of the examples above did the word, and, occur. This makes them considerably easier to test, as youre not forced to juggle multiple objects.
Tip: Reduce each class to being responsible for one thing. This is referred to as The Single Responsibility Principle.
Definition: Polymorphism refers to the act of breaking a complex class into sub-classes which share a common interface, but can have unique functionality.
The easiest possible way to determine if a class could benefit from polymorphism is to hunt down switch statements (or too many repeated conditionals). Imagine a bank account class that should calculate yearly interest differently, based on whether the type of account is checking, savings, or some other type entirely. Heres an incredibly simplified example:
https://tutsplus.com/2012/04/the-aha-moment/
15
function addYearlyInterest($balance) { switch ($this->accountType) { case 'checking': $rate = $this->getCheckingInterestRate(); break; case 'savings': $rate = $this->getSavingsInterestRate(); break; // other types of accounts here } return $balance + ($balance * $rate); }
In situations such as this, a smarter course of action is to extract this similar, but unique logic to sub-classes. Define an interface to ensure that you have access to a getRate method. Youll often hear interfaces referred to as contracts. This is a nice way to think of them: any implementation, according to the terms of the contract, must implement the given methods.
1 2 3
16
Now, the original method can be cleaned up considerably. Notice how weve type-hinted the $interest variable below. This provides us with some protection, as we dont want to fall into a trap of calling a getRate method on a class, if it doesnt exist. This is precisely why weve coded to an interface. By implementing the interface, the class is forced to offer a getRate method.
1 2 3 4 5 6 7 8 9 10
function addYearlyInterest($balance, BankInterestInterface $interest) { $rate = $interest->getRate(); return $balance + ($balance * $rate); } $bank = new BankAccount; $bank->addYearlyInterest(100, new CheckingInterest); // 101 $bank->addYearlyInterest(100, new SavingsInterest); // 103
Even better, testing this code is a cinch, now that we no longer need to account for multiple paths through the function. Again, dont sweat over the syntax. Well cover it soon enough.
1 2 3 4 5 6 7 8 9 10
public function testAddYearlyInterest() { $interest = Mockery::mock('BankInterestInterface'); $interest->shouldReceive('getRate')->once()->andReturn(.03); $bank = new BankAccount; $newBalance = $bank->addYearlyInterest(100, $interest); $this->assertEquals(103, $newBalance); }
Tip: Polymorphism allows you to split complex classes into small chunks, often referred to as sub-classes. Remember: the smaller the class, the easier it is to test.
17
Definition: Coupling refers to the degree in which two components in your system are dependent upon one another. If removing one affects the other, then youve unfortunately written tighly coupled code that isnt easy to change.
As Ben beautifully put it, if there was a bug on line seven, then, chances are, theres also a bug on line eleven. Nip that in the bud as early as possible.
Tip: When presented with such bugs, begin asking yourself how you can split the logic up into smaller (easier to test) classes. In addition to improved testability, one perk to this pattern is that it allows for significantly more readable production code.
18
Test Jargon
Im not so self-consumed to think that this book will serve as your sole source of testing education (I certainly hope it isnt). If youre anything like myself, youll find yourself scouring the web late at night for every last fragment of education to fill in those missing pieces in your understanding. In the process, youll come across incredibly confusing jargon. Worse, this terminology is inconsistent from language to language! Yikes! In this book - and in the spirit of simplicity - well break things down into their simplest terms. Scan the following definitions, but dont feel that you must commit them to memory all at once. In truth, in many ways, Im very much against all this confusing jargon. If a term doesnt immediately make sense, then it should be changed. The development community should take its cues from astrophysics. The most accessible field in science, from the point of view of language, is astrophysics. What do you call spots on the sun? Sunspots. Regions of space you fall into and you dont come out of? Black holes. Big red stars? Red giants. So I take my fellow scientists to task. Hell use his word, and if I understand it, Ill say, Oh, does that mean da-dada-de-da? - Neil Degrasse Tyson
Unit Testing
Think of unit testing as going over your classes and methods with a fine-tooth comb, ensuring that each piece of code works exactly like you expect. Unit tests should be executed in isolation, to make the process of debugging as easy as possible. 80% of your tests will be in this style. If it helps, when you think of unit testing, think one object, and one object only. If a test fails, you know exactly where to look.
Model Testing
Some members of the Ruby on Rails community associate model testing (even when these tests touch the database) with unit testing. This unfortunately can be a bit misleading. Unit tests should be isolated from all external dependencies. Once you ignore this basic rule, youre no longer unit testing. Youre writing integration tests (more on that shortly). In this book, when we test our models, well stay true to the traditional definition of unit testing, unless specified otherwise. Imagine that a method in your model is responsible for sending an email. If following good design patterns, youll likely have a class that is dedicated to sending email (single responsibility principle). This presents a problem, however: how do we successfully unit test this method, if it calls an external Mailer class? The answer is to use mocks, which well cover extensively in this book. A mock allows us to fake the Mailer class, and write an expectation to ensure that the proper method is called. This
19
way, even if the Mailer component is currently broken (it will have its own tests), we can still verify whether or not the model method is working as expected.
Integration Testing
If a unit test verifies that code works correctly in isolation, then an integration test will fall on the other end of the spectrum. These tests will flex multiple parts of your application, and typically wont rely on mocks or stubs. As such, be sure to create a special test database. As an example, think of a car. Sure, the engine and fuel injection system might individually work as expected (each passes its own set of unit tests), but will they work when grouped together? Integration testing verifies this.
Acceptance Testing
Youve already learned that functional testing ensures that the code meets the requirements of the development team. However, there will be cases when, even though the tests return green, the final implemented feature will not meet the requirements of the client. This is what we refer to as acceptance testing. In other words, does this code meet the requirements of the client? Your software can pass all unit, functional, and integration tests, but still fail the acceptance tests, if the client or customer realizes that the feature doesnt work as they expected.
Tip: If functional tests meet a developers assumptions and requirements, acceptance tests are intended to verify the clients expectations.
Think of Tuts+ Premium, the subscription-based technical education service that I work for. Recently, we added a new bookmarking feature that allows you to save courses and eBooks that
http://tutsplus.com
20
you want to read later. Before the developers can begin writing a single line of code, they first need to understand what the content teams expectations are - theyre the ones requesting the feature. This requires an acceptance test.
1 2 3
In order to keep track of what to learn next As a member I want to bookmark content
Once this acceptance test passes, it may be assumed that the feature has fully been implemented, and meets the clients (the content team, in this case) expectations.
In the final section of this book, youll learn how to write acceptance tests using the Codeception framework. Youll find that this allows us to use human speak to define how we want to interact with our applications.
21
the sole purpose of providing you with more security. Is there merit to this argument? No, no theres not. In fact, plenty of studies have found that a testdriven development cycle reduces the length of time it takes to complete a project.
Relax
Ill be the first one to tell you that these definitions took me a very long time to learn and appreciate. I certainly dont expect this to stick after the first reading. Sheesh; we havent even gotten to the Intro to PHPUnit chapter! For now, simply keep in mind that, as you develop applications, youll make use of multiple styles of testing. Having said that, the unfortunate truth is that the development community, as a whole, cant seem to agree on terminology to save their lives. Youll also comes across terms, like system testing, request specs, medium tests, and more. In most cases, theres close overlap between these terms and the ones referenced earlier. Dont worry about this too much; the most important thing is to get you testing. Youll develop your own style in time. As the saying goes, it doesnt matter how you testjust as long as you do test. The dissonance continues beyond terminology. While its fair to say that most developers these days agree that writing tests is vital, in what order those tests are written is a different story. Some evangelists, like Bob Martin (Uncle Bob), recommend strict adherence to the TDD philosophy: do not write a single line of production code until youve first written a test. It has become infeasible for a software developer to consider himself professional if he does not practice test-driven development. - Bob Martin But, other equally influential developers, like DHH (creator of Ruby on Rails), freely admit that they write the tests after the production code - roughly 80% of the time. Dont force yourself to test-first every controller, model, and view (my ratio is typically 20% test-first, 80% test-after). - David Heinemeier Hansson Its your job to take in all of the input and advice around the web, and mold that into a style that you (or your development team) can embrace. As such, view this book less as a Bible, and more as one persons adaptation of testing, that you can then morph to your style. There is no spoon.
http://www.youtube.com/watch?feature=player_detailpage&v=KtHQGs3zFAM#t=77s http://37signals.com/svn/posts/3159-testing-like-the-tsa
Installation
At the time of this writing, there are a few different ways to install PHPUnit on your system, including: Pear - No one on the planet has ever enjoyed Pear. Composer - Installs PHPUnit on a per-project basis. PHAR - Think of this one as a one-click archive. Youre certainly free to choose the method you wish, however, as well heavily be leveraging Laravel and Composer in this book, well take the simplest route, and add PHPUnit as a development dependency to the composer.json file of our Laravel application.
Wait, whats this Composer thing? Its the PHP communitys preferred tool for dependency management. It provides an easy way to declare a projects dependencies, and pull them in with a single command. As a Laravel 4 developer, its vital that, before moving forward, you have a basic understanding of what Composer is, and how to use it.
Before we dive into a Laravel 4 project, lets begin with the absolute basics. When learning new technologies, libraries, or frameworks, always strip the examples down to the bare essentials. This is the best way to learn as quickly as possible.
http://www.phpunit.de/manual/3.8/en/index.html http://getcomposer.org
22
23
Create a new folder, called lesson. Within it, add a new composer.json file, and append the following:
1 2 3 4 5
Notice how were not adding the dependency to the require object. As PHPUnit is only necessary during development, we dont need to worry about the production server pulling in this package. The next step is to install it and any other dependencies that you might have registered in your application. From the command line, assuming that Composer is installed globally on your system, run:
1
Give that command a moment to process, and, if all goes according to plan, you should now have access to both vendor/bin/phpunit and vendor/phpunit/. Go ahead and try it out by viewing a list of PHPUnits commands. From the root of your project, run:
1
vendor/bin/phpunit -h
24
If that output feels overwhelming, dont worry. Like Git, a basic understanding of these various commands can carry you a long way.
Tip: You certainly dont want to type vendor/bin/phpunit every time you need to run your tests. Add vendor/bin to your path, so that you can simply type phpunit.
Dont forget that aliases are your friend. To call PHPUnit with a single key, on a Mac, try: alias t="vendor/bin/phpunit". This is a temporary alias, but can easily be added to /.bash_profile.
25
Next, add the full path to the projects vendor/bin directory to your path. Heres an example for my Unix-based system. This can be added to your /.bash_profile or /.zshrc, if you use oh-myzsh.
1
export PATH=/Users/Jeffrey/composer-packages/vendor/bin
With this technique, PHPUnit will always be available. Even better, updating these global packages only requires a simple composer update.
While installing PHPUnit directly through Composer is certainly a valid route - and will do just fine for the purposes of this book - some developers prefer to stick with Pear (I have no idea why). If you fit that description, feel free to do so. It makes zero difference.
Assertions 101
Its time for the hello world of testing. In this section, youll be introduced to your first PHPUnit assertion, assertTrue. Add a new file, called PracticeTest.php, to a tests folder within your project. Heres some boilerplate to get you started.
1 2 3 4 5 6 7 8 9 10
<?php // lesson/tests/PracticeTest.php class PracticeTest extends PHPUnit_Framework_TestCase { public function testHelloWorld() { $greeting = 'Hello, World.'; $this->assertTrue($greeting === 'Hello, World.'); } }
Before we decode it, run the test to ensure that it works as expected.
https://github.com/robbyrussell/oh-my-zsh
26
$ phpunit tests PHPUnit 3.7.18 by Sebastian Bergmann. . Time: 0 seconds, Memory: 2.50Mb OK (1 test, 1 assertion)
Because we have not yet applied any configuration options, we must specify a path to where the tests for our project are located. PHPUnit will tunnel through this folder recursively, searching for test files. This means that youre free to organize the tests folder structure however you wish. For now, though, keep things simple, and store all test files in the same space. First, notice the final line, OK (1 test, 1 assertion). This, to state the obvious, reveals that one test passed successfully. Did you notice the single period in the output? Thats the representation of a single test. When you add a second test, then two periods will be displayed if they both pass, of course. In the event that the test fails, instead of a period, youll find a big fat F, for failure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$ phpunit --colors tests PHPUnit 3.7.18 by Sebastian Bergmann. F Time: 0 seconds, Memory: 2.75Mb There was 1 failure: 1) PracticeTest::testHelloWorld Hello, World. Failed asserting that true is false. /Users/Jeffrey/Desktop/chapter-1/tests/PracticeTest.php:8
The majority of your days will be spent staring at failing tests, so get used to it! Luckily, the output will prove incredibly helpful. Above, we can see that there was one failure from the testHelloWorld test, and specifically on line eight. Helpful!
Where Are The Colors? In the next chapter, well dig more deeply into PHPUnits configuration options. Until then, use the --colors option to request colored output: vendor/bin/phpunit --colors tests.
27
<?php // lesson/tests/PracticeTest.php class PracticeTest extends PHPUnit_Framework_TestCase { public function testHelloWorld() { $greeting = 'Hello, World.'; $this->assertTrue($greeting === 'Hello, World.', $greeting); } }
File Naming - The file name is important. Notice that were following a FooTest.php convention. While this can be modified, lets stick with the defaults for now. Convention over configuration, right? Matching - The name of the class is the same as the file name. Inheritance - The class extends PHPUnit_Framework_TestCase. This class was made available when we installed PHPUnit through Composer. Youll soon find, however, that, when testing in Laravel, we typically extend Laravels TestCase (the other file within the tests/ folder). If you move up the inheritance chain, youll find that a parent class does extend PHPUnit_Framework_TestCase. We simply inherit from TestCase to set up some Laravelspecific processes, as well as pull in a few helper methods for testing our applications. Method Naming - Every test should be contained within a method that has a descriptive name, and begins with the word, test. Take a few moments, and commit some of these techniques to memory. Lets next investigate the test logic, itself.
assertTrue
1 2
Hopefully, youll find that PHPUnits test assertions are quite readable! Even without understanding the nuts and bolts, its easy to decipher whats happening here. All assertions are available on the test class instance. In this case, we want to assert that the value of the $greeting variable is, in fact, equal to Hello, World. The assertTrue method accepts two parameters.
28
As youll find in PHPUnit, inverse methods are nearly always available. Should you need to assert that a value is, not true, but false, then assertFalse has you covered.
1
assertEquals
In the previous example, our only goal was to assert that a variables value was equal to a specified string. While assertTrue will get the job done, its not the most readable option in this case. Never ignore test readability. Itll eventually bite you, if you do. Lets introduce another assertion that is a better fit for this task: assertEquals.
1 2
Thats better, isnt it? Similar to most of PHPUnits assertions, assertEquals accepts three arguments:
1
$this->assertEquals(EXPECTED, ACTUAL, OPTIONAL MESSAGE); assertNotEquals is the inverse of this assertion, and has the same signature.
If you wish to prove that two values are equal to one another, then, clearly, assertEquals is a better choice than assertTrue, even though both will work. Run the tests by returning to the command line and calling PHPUnit:
1
$ phpunit
29
Your Turn: Make a variable, $sum, equal to 2 + 2. Next, write an assertion to prove that $sum does, in fact, equal 4. For bonus points, write the assertion first.
assertSame
assertEquals will break down as soon as you need to compare with strict equality. For instance, how might you assert that a variable is equal to 0? You might try:
1 2
This will work; however, should you subsitute any other falsy value, the tests will still return green.
30
In situations, when you require strict comparison (or effectively ===), reach for assertSame, as demonstrated below:
1 2 3 4 5
assertContains
The goal of this book isnt to teach every assertion, however, its important to cover the essentials. While PHPUnit offers dozens of assertions, youll likely find that even a handful of them will serve the huge majority of your testing needs. In the instances where a piece of code requires a different form of assertion, PHPUnit is heavily documented. Imagine that you have a list of names, and need to prove that the array contains a specific value. Again, while assertTrue can handle this task, its better to opt for the more readable option: assertContains.
1 2 3 4 5
An easy way to remember the argument order for these various assertions is to read them aloud. Assert contains Dayle, given this HAYSTACK is more readable than Assert contains HAYSTACK, and the item, Dayle.
As you are now aware, the inverse assertion is also available. Lets ensure that a troll is not included in our list of influential Laravel developers.
31
assertArrayHasKey
There will be times when you need to assert that a provided array contains, not a specific value, but a key. For instance:
1 2 3 4
In this pseudo-example, lets assume that we require a $family to contain a parent. In situations such as this, assertContains is not the right tool for the job.
1 2
Instead, we need to assert that a key exists within the specified array. The solution is assertArrayHasKey.
1 2 3 4 5 6 7 8 9
public function testFamilyRequiresParent() { $family = [ 'parents' => 'Joe', 'children' => ['Timmy', 'Suzy'] ]; $this->assertArrayHasKey('parents', $family); // true }
To take things a step further, what if we expect the parents key to contain an array of one or more parents? How might we do that?
assertInternalType
assertInternalType can be used to verify the type of the supplied variable.
1
Continuing on with the family example, to assert that a familys parents key is equal to an array of one or more items, we might do:
32
public function testFamilyRequiresParent() { $family = [ 'parents' => 'Joe', 'children' => ['Timmy', 'Suzy'] ]; $this->assertInternalType('array', $family['parents']); // false }
assertInstanceOf
Often, you will need to ensure that a variable is an instance of some class. This is easy in PHPUnit, via the assertInstanceOf method.
1
class DateFormatter { protected $stamp; public function __construct(DateTime $stamp) { $this->stamp = $stamp; } public function getStamp() { return $this->stamp; } }
To ensure that $stamp is an instance of PHPs DateTime class (other than the fact that weve provided type hints), we could use the following test:
33
public function testStampMustBeInstanceOfDateTime() { $date = new DateFormatter(new DateTime); $this->assertInstanceOf('DateTime', $date->getStamp()); // true }
Asserting Exceptions
When unit testing, its important to test every possible path through your code. This is one of the core reasons why, if you use too many conditionals, it can make a class or method difficult to test. An example of such a path might be one that throws an exception. In PHPUnit, we use doc-blocks to assert exceptions, like so:
1 2 3
This doc-block declares that, given the contents of the method that it corresponds to, PHPUnit should expect an exception to be thrown. If one is not, then the test will fail. Lets say that a method should throw an exception if a non-numeric value is passed. Heres how we might assert that:
1 2 3 4 5 6 7 8
/** * @expectedException InvalidArgumentException */ public function testCalculatesCommission() { $commission = new Commission; $commission->setSalePrice('fifteen dollars'); }
Notice how, in this case, there is no assertX call. Instead, weve merely added the necessary code that should make the class throw an exception.
Summary
While we could continue on our way, reviewing every available PHPUnit assertion, this would be unwise. Our brains are leaky little things; fill them up too quickly, and they burst. The handful of
34
assertions covered in this chapter should be enough to get you on your way. When you do need additional functionality, the documentation is only a click away. Next, before we dive into Laravel-specific testing, we should take some time to configure PHPUnit more fully to our needs.
Options
Technicolor
Though youre free to live life in black and white, its recommended that you instead enable colored output when running tests. Similar to a traffic stop light, a simple green versus red output can help to speed up your development workflow. We respond to color more quickly than text.
1
phpunit --colors
35
36
37
Bootstrapping
There will be times when you need to include certain files before executing your tests. A perfect example of this is ensuring that Composers autoload script (located at vendor/autoload.php) is included before any tests run. Though you could manually require this file either at the beginning of every test class or within a master file, its better to leverage PHPUnits bootstrap switch, like so:
1
phpunit --bootstrap="vendor/autoload.php"
38
Output Formats
In addition to the default format for test reporting, PHPUnit offers two alternative flags: --tap and
--testdox.
TAP, an acronym for Test Anything Protocol, is Perls text-based interface between testing modules.
1 2 3 4 5
The TestDox format takes a slightly different approach. It will read your test methods and convert them from camelCase names to readable sentences. As an example, testRedirectsToHomePageOnSave will be converted to Redirects To Home Page On Save. See below:
1 2 3 4 5 6
$ phpunit --testdox tests PHPUnit 3.7.18 by Sebastian Bergmann. Practice [x] Reloads current page if save fails [x] Redirects to home page on save
This can be helpful for a birds-eye view of which tests were successful, as well as providing documentation. Remember, thats one of the significant advantages to writing tests: free documentation!
Instead, leverage PHPUnits ability to read a configuration file. Lets review the simplest possible example. Within the root of your project, create a phpunit.xml file and insert:
1
<phpunit colors="true"></phpunit>
With that single line, you may now exclude the --colors flag from your tests. However, lets flesh this file out a bit more, using some options that you havent yet seen.
39
The first two attributes should already be familiar to you. The remainder are optional, and boil down to personal choice. I tend to prefer that all errors, notices, and warnings be converted to exceptions. Also, if a test fails, then I want to cancel any remaining tests. Think of it as Control + C on failure. Though some may disagree, as I see it, theres no use in executing potentially hundreds of tests if youre only concerned with fixing the first failure. As things currently stand, we still must manually specify the path to the tests/ directory. Thats no good! To set a custom directory, use the <testsuites> element along with any number of child <testsuite> elements, like so:
1 2 3 4 5 6 7 8 9 10 11 12 13
<phpunit bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="true"> <testsuites> <testsuite name="Test Suite"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
Splitting your test suites in this way can add a great deal of flexibility down the line, when you only wish to test a particular subset of your application. Lets add another test suite that is specifically for, say, integration tests.
40
<testsuites> <testsuite name="Test Suite"> <directory>tests</directory> </testsuite> <testsuite name="Integration"> <directory>tests/integration</directory> </testsuite> </testsuites>
To execute only the tests referenced in the Integration test suite, run:
1
phpunit --testsuite="Integration"
For most chapters in this book, well stick with one master test suite, however, for real-world projects, be sure to organize your tests. On save, you dont want to be executing acceptance tests, in addition to the unit tests. Nonetheless, to execute all tests within the tests/ folder while passing the options listed above, simply run phpunit. Success!
Did You Know? Laravel ships with its own phpunit.xml file (located in the root of your application). Though it may freely be modified, most of the time, youll find that the default settings are perfect for your needs. As such, manually creating this file is only necessary for non-Laravel projects.
Continuous Testing
An excellent way to get into the testing groove is to automate the process of running your tests each time an applicable file is saved. While light-weight editors, like Sublime Text, dont offer this functionality out of the box (most IDEs do), its a fairly simple process to install a few command-line tools to get the job done. Please note that, to leverage the following tools, youll need to install Ruby and Rubygems. Mac users can follow this guide, while Windows faithfuls can use the simple Ruby Installer.
http://net.tutsplus.com/tutorials/ruby/how-to-install-ruby-on-a-mac/ http://rubyinstaller.org/
41
RubyGems is the Ruby communitys gem hosting service, not dissimilar to Composers Packagist companion.
If you like the idea of automated testing, I recommend using Guard to handle your continuous testing needs, along with the notification tool that best suits your needs and OS.
Definition: Guard is a command line tool to easily handle events on file system modifications.
To get setup, begin by installing Guard and the PHPUnit plugin through the command line.
1 2
Pay close attention to the output upon running these commands. Its possible that youll be advised to install additional packages, such as rb-fsevent. If so, follow orders!
1
Lastly, if you desire notifications (essentially a PASS/FAIL banner on save), Mac users should install the Terminal Notifier Guard gem.
1
With that, youre all set to go! Initialize Guard for your application by running:
1 2 3 4 5
guard init 19:15:57 - INFO - Writing new Guardfile to /Users/Jeffrey/Desktop/demo/Guar\ dfile 19:15:57 - INFO - phpunit guard added to Guardfile, feel free to edit it
Assuming that guard-phpunit is the only Guard plugin that you have installed, running this command will generate a file, called Guardfile, written in Ruby, containing the following boilerplate:
http://rubygems.org https://github.com/guard/guard https://github.com/Springest/terminal-notifier-guard
42
# A sample Guardfile # More info at https://github.com/guard/guard#readme guard 'phpunit', :cli => '--colors' do watch(%r{^.+Test.php$}) end
If youre new to Ruby, dont let this code scare you; its really quite simple. It specifies that we want Guard to watch all files that match the regular expression, .+Test\.php$, or, in human speak, any file in the root of the project that ends with Test.php. Lets adjust this somewhat to reflect a more realistic folder structure.
1 2 3
guard 'phpunit', :cli => '--colors', :tests_path => 'tests' do watch(%r{^.+Test.php$}) end
Using the tests_path option, weve specified that the tests/ path should be used when triggering all tests. Give it a try: create a simple tests/ExampleTest.php file, and add:
1 2 3 4 5 6 7 8
<?php // tests/ExampleTest.php class ExampleTest extends PHPUnit_Framework_TestCase { public function testContinuousTesting() { $this->assertTrue(false); } }
Now Guard doesnt magically run automatically. You must tell it when to begin watching files, using the guard command.
1 2 3 4 5 6 7 8 9 10
$ guard 19:41:15 - INFO 19:41:15 - INFO 19:41:15 - INFO 19:41:15 - INFO > [^C5AD6455F02F] > [^C5AD6455F02F] > [^C5AD6455F02F] > [^C5AD6455F02F] > [^C5AD6455F02F]
Guard uses TerminalNotifier to send notifications. Guard uses TerminalTitle to send notifications. Running all tests F Failures: 1) ExampleTest::testContinuousTesting Failed asserting that false is true.
43
[^C5AD6455F02F]
# /Users/Jeffrey/Desktop/demo/tests/ExampleTest.php:\
If everything went according to plan, youll see something along the lines of Figure 2.3. Continuous testing for the win!
As long as a file within the tests/ folder that matches the regular expression, .+Test\.php$, is modified, the tests will run again. Make a few changes to tests/ExampleTest.php, save, and see for yourself. If you return to the command line, to re-run all tests, hit Enter. Alternatively, to cancel Guard, type exit, or simply e.
Watching Files
Chances are high that you want Guard to monitor more than merely test files. If following a TDD cycle, youll surely need a way to trigger a class associated test upon save. Heres an updated Guardfile that creates a link between Calculator.php and CalculatorTest.php.
1 2 3 4 5 6
guard 'phpunit', :cli => '--colors', :tests_path => 'tests' do watch(%r{^.+Test.php$}) watch('app/libraries/Calculator.php') { 'tests/libraries/CalculatorTest.p\ hp'} end
44
To verbalize this new line of code: when Calculator.php is modified, then run, not all tests, but specifically the one at tests/libraries/CalculatorTest.php. However, we clearly dont want to repeat this step for every single file in our project! Lets instead use regular expressions to make the matching more dynamic.
1 2 3 4 5 6
guard 'phpunit', :cli => '--colors', :tests_path => 'tests' do watch(%r{^.+Test.php$}) watch(%r{app/libraries/(.+).php}) { |m| "tests/libraries/#{m[1]}Test.php"\ } end
With this modification, any file within the app/libraries directory will trigger its associated test.
Tip: Notice how the folder structure for our tests mirror the app structure. This is widely considered to be a best practice.
This takes care of our library tests, however, to extend it even further, we might update the Guardfile to:
1 2 3 4 5
guard 'phpunit', :cli => '--colors', :tests_path => 'tests' do watch(%r{^.+Test.php$}) watch(%r{app/(.+)/(.+).php}) { |m| "tests/#{m[1]}/#{m[2]}Test.php" } end
Now, the subfolder matching will be dynamic as well. When the file, app/models/User.php, is saved, Guard will now attempt to run the test located in tests/models/UserTest.php.
45
guard 'phpunit', :cli => '--colors', :tests_path => 'tests' do watch(%r{^.+Test.php$}) watch(%r{^app/views/.+$}) { Dir.glob('tests/\**/*Test.php') } end
With this last modification, if any file within the app/views folder is saved, Guard will trigger all *Test.php files within the tests/ directory. Above, were using Dir.glob() to return an array of all appropriate files. Its admittedly not the most elegant solution, but, until the Guard PHPUnit plugin offers a better solution, this is the best option.
Tip: Remember, Guard isnt limited to PHPUnit by any stretch. It can additionally be used to compile Sass and CoffeeScript, reload the browser, and much more. Refer to the Guard Wiki for a full list of available plugins.
nmap ,t :!phpunit<cr>
This specifies that, when I type ,t, the phpunit command should be executed (<cr> represents a carriage return). Ive found that, for my workflow, this makes for the cleanest TDD cycle - one that Im in complete control over. While the previous command is excellent for triggering the full test-suite, when testing in isolation, its better to exclusively target the applicable class. In these cases, I manually create mappings on the fly to override the default behavior of ,t. A first option might be:
1
The only difference with this mapping is that PHPUnit will exclusively test the current class (represented by the percentage sign). Though this works, it assumes that the mapping is triggered from the test class. Chances are, though, that, as a Vim user, youll be using split panes (multiple windows). A better approach is to hardcode the file that youre currently working on.
https://github.com/guard/guard/wiki/List-of-available-Guards
46
Now, regardless of which split pane you might be in, running ,t will correctly trigger the tests located in MyClassTest.php. Success! You now have a simple key stroke for testing the class. Dont forget that you can press the up arrow key to cycle through your previous commands. This way, you dont have to manually type the same sequence over and over.
Summary
In this chapter, you were introduced to the basics of configuring PHPUnit. For language-agnostic projects, an understanding of these various options will prove essential. However, as noted earlier, when testing Laravel applications, dont forget that the framework provides its own phpunit.xml file out of the box. This file is by no means read-only. If you need to add a new test suite (perhaps for your acceptance tests), edit the file as needed. And, with that, were on to the next chapter. Go have some celebratory raisins; youre doing great.
If you installed PHPUnit through Composer, then the list of functions will be stored in:
1
vendor/phpunit/phpunit/PHPUnit/Framework/Assert/Functions.php
Simply require that file at the top of your test class (or at the top of Laravels TestCase), and you may now reference these assertions as global functions.
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php require_once 'vendor/phpunit/phpunit/PHPUnit/Framework/Assert/Functions.php\ '; class PracticeTest extends TestCase { public function testAdd() { $sum = add(10, 5); assertEquals(15, $sum); } }
47
48
The only problem is that, when it comes to testing, were still limited to PHPUnits verbose syntax that doesnt blend with Laravel as elegantly as we might hope. This is why I recently released two wrappers classes around PHPUnits assertion library that bring the Laravel feel to your testing: Should and Assert. With these two wrappers, which can be downloaded through Composer, your assertions will take the form of:
1 2 3 4
Behind the scenes, the applicable PHPUnit assertions are still being called. These wrappers simply provide a more expressive interface for writing tests. Though I wont be using this package in this book, if youd like to use it in your own projects, update your composer.json to reference the phpunit-wrappers package, like so:
1 2 3 4 5
Next, run composer install --dev or composer update --dev to download the package from GitHub into your vendor directory. Finally, import the Should or Assert class (or both) into your test file, and have fun!
https://github.com/JeffreyWay/Laravel-Test-Helpers
49
<?php use Way\Tests\Assert; use Way\Tests\Should; class PracticeTest extends TestCase { public function testItWorks() { $name = 'Joe'; Should::equal('Joe', $name); Assert::equals('Joe', $name); } }
If you want to create a custom alias for a PHPUnit assertion, doing so is a cinch.
1 2 3
Now, when you call Should::eq(), behind the scenes, the arguments that you pass to it will be sent through to PHPUnits native assertEquals method. Nifty!
Please note that, while I favor many of the tricks outlined in this chapter, for the purposes of the book, they wont be used. Lets stick with the standard API.
My Struggles
One unfortunate truth of learning how to test is that so much of the information on the web and in books contradicts one another. As a student, Id read one article, think I had a decent grasp on the terminology and rules, and then move on to the next article that, in many ways, preached an entirely different set of guidelines. Imagine learning what two plus two equals, when all the resources provide different answers? Whats the reason for this dissonance in the community? Well, its really quite simple: were still figuring this stuff out! Even though the concept of testing software goes as far back as the seventies - during the days of Smalltalk - the truth is that, even today, the development community remains divided on many issues, including such basic things as whether testing is beneficial. As a result, as you continue learning beyond this book, be prepared to discover different methodologies and terminologies, dependent upon a number of factors, including the articles publish date, which language it was written for, and what belief system its writer has. The differing view points were only the beginning of my struggles, though. As I continued digging in, I didnt yet understand or appreciate the various types of testing. As such, Id learn from one source that testing in isolation was paramount, and then move on to find countless other articles, where the authors seemed to ignore this rule, in favor of touching every part of the system. Of course, now, I realize that these articles focused on different types of testing (acceptance or functional vs. unit), but, back then, I was dumbfounded and overwhelmed. I suppose what I want you to understand and appreciate is that were still in the early days of testdriven development, and testing in general. Even worse, while the Ruby on Rails community is a culture that has widely embraced and evangelized testing, the same unfortunately cant be said for the PHP world. But, were fixing that, right? The tides are turning; I hope youre on board! In this chapter, well focus exclusively on unit testing with test-driven development. If youve come across other types of testing in your web travels - perhaps ones that do hit databases and query APIs - rest assured that they absolutely have their place. In fact, in real world projects, youll have a number of test suites, each which exercises your application in slightly different ways. That being said, this chapter is not concerned with those other styles of testing; well get to them in due time. For now, though, and as a first step into the testing waters, lets focus exclusively on one object at a time.
http://en.wikipedia.org/wiki/Smalltalk
50
51
Unit Testing
Okay, its time to dig into the process of unit testing, not our models or controllers (well get to those soon), but simple functions and utility classes. This will give us the perfect excuse to learn more about proper test organization.
Unit testing refers to the smallest testable piece of an application. If I feed a method this set of data, then, in return, I expect that. Nothing beyond that class should be touched (if were following the London School of TDD). Not the database, not a single dependency. This is referred to as testing in isolation.
public function testFetchesItemsInArrayUntilKey() { // Arrange $names = ['Taylor', 'Dayle', 'Matthew', 'Shawn', 'Neil']; // Act $result = array_until('Matthew', $names); // Assert $expected = ['Taylor', 'Dayle']; $this->assertEquals($expected, $result); }
If you pick up a BDD-specific book, however, you might come across slightly different terminology that translates to the same thing: Given, When, Then. Given this set of data, when I perform this action, then I expect that response. Heres a second test for illustration purposes:
52
/** * @expectedException InvalidArgumentException */ public function testThrowsExceptionIfKeyDoesNotExist() { // Given this set of data $names = ['Taylor', 'Dayle', 'Matthew', 'Shawn', 'Neil']; // When I call the until function and // specify a different key $result = array_until('Bob', $names); // Then an exception should be thrown (see doc-block) }
Though the terminology is different, this Given/When/Then syntax is a bit more readable, and encourages you to describe the behavior of your code. Also, note that each step in the cycle is quite small. If the ratio becomes lopsided, its a good indication that refactoring is in order. Finally, if youre curious, heres the code to make both tests pass:
1 2 3 4 5 6 7 8 9 10 11
function array_until($stopPoint, $arr) { $index = array_search($stopPoint, $arr); if (false === $index) { throw new InvalidArgumentException('Key does not exist in array'); } return array_slice($arr, 0, $index); }
Testing in Isolation
A core fundamental to successful unit testing is to test in isolation. This means that all outside dependencies which arent directly related to the thing that youre attempting to test should be stubbed or mocked (more on these terms later). For example, when unit testing a model, you shouldnt, in the process, be hitting the database. You shouldnt hit a web service. You shouldnt even reference one of your other classes. Instead, stick with one object at a time. You can flex multiple parts of the system in the future, when you write integration tests.
53
Tip: Each test should recreate the world in which it acts upon. Or, in other words, never depend on test order.
Test-Driven Development
Test-Driven Development is an agile software pattern in which a developer prepares a test before a single line of production code is written. Popularized by Kent Beck, in addition to ensuring that your code works as expected, this methodology forces you to think before coding. While a cowboy approach may once have been the norm in our industry, as a collective, were rapidly moving away from it, in favor of a mature, intentional development process. TDD declares three core rules: 1. Write a Failing Test: You may not write a single line of production code unless a failing test is present. 2. Make it Pass: Once a test has been defined, you may only write the minimum amount of code to make the test pass - even if this means faking a methods return value. Approach each failing test from the perspective of, what is the simplest way to make this test pass? 3. Refactor: Only when the tests have passed (they return green) may you refactor your code.
Behavior-Driven Development
In addition to TDD, or test-driven development, youll often hear about a seemingly different form of testing, referred to as behavior-driven development. As it turns out, if you write TDD well - by describing the behavior of your code - then chances are, youre following the rules of BDD. With this style of coding, you describe how the SUT (system under test) should behave. Once youve written
54
the minimum amount of code to make that behavior work, then the feature has been implemented. In this book, when I refer to TDD, Im also referring to BDD. The two are not mutually exclusive. One is merely the other in its best form.
Testing Functions
Before we tackle the calculator, first, lets test-drive a simple function. In real-world applications, youll be testing logic from external sources, such as a function, class, or web service. To take a first step into these waters, well test a helper function for a Laravel application that should generate an anchor tag, called link_to. In fact, Laravel 4 recently implemented this very function. Nonetheless, lets rebuild it for education purposes. If working along, youll want to rename it to something that doesnt clash with the existing version. Additionally, well embrace a TDD (test-driven development) approach, and write the test first.
1 2 3 4 5 6 7
public function testGeneratesAnchorTag() { $actual = link_to('dogs/1', 'Show Dog'); $expect = "<a href='http://:/dogs/1'>Show Dog</a>"; $this->assertEquals($expect, $actual); }
Hopefully, this is beginning to make sense. Notice how we interact with the function before writing any production code. This way, were never constrained by existing code. Or, in other words, how would you want to achieve this functionality in a perfect world? Write it as such, and then its your responsibility to organize the necessary production code to make that possible. In this case, we need a link_to(URL, BODY) function that should generate the HTML string that is stored in the $expect variable above.
1
Remember that we shouldnt be searching for localhost:8000, when running command line tests. Then, we write the assertion and run the test.
55
$expect = "<a href='http://:/dogs/1'>Show Dog</a>"; $actual = link_to('dogs/1', 'Show Dog'); $this->assertEquals($expect, $actual);
phpunit
The wonderful thing about TDD is that it turns your coding process into a game. Not sure what to do next? Run the tests, and PHPUnit will tell you! TDD is about tiny, tiny steps. In this case, an error is displayed: Call to undefined function link_to(). Theres our next step! Often, youll find that its helpful to write the class or function under test just above the test class, like so:
56
function link_to() {} class FunctionsTest extends PHPUnit_Framework_TestCase { public function testBuildsAnchorTag() { $actual = link_to('dogs/1', 'Show Dog'); $expect = "<a href='http://:/dogs/1'>Show Dog</a>"; $this->assertEquals($expect, $actual); } }
Having said that, to mimic a real-world scenario, this time, well take the latter approach. Lets assume that we have a helpers.php file within the app/ folder. Think of this file as one that contains a variety of simple global functions, which can be used throughout your application. Begin by creating the file, app/helpers.php.
1 2 3 4 5
Though we could simply require this file within the test class (and probably should at this early stage), lets instead try to embrace autoloading. Composer isnt a mind reader, though; we need to instruct it to load this new file. Luckily, thats a cinch. Return to composer.json and specify a new files array within the autoload object. Heres how your Composer file should look:
57
// composer.json { "require": { "laravel/framework": "4.0.*" }, "require-dev": { "phpunit/phpunit": "3.7.*" }, "autoload": { "classmap": [ "app/commands", "app/controllers", "app/models", "app/database/migrations", "app/database/seeds", "app/tests/TestCase.php" ], "files": [ "app/helpers.php" ] }, "minimum-stability": "dev" }
Remember: we cant add app/helpers.php to the classmap, as its not a class. In these situations, the best choice is to instead use the files array, as illustrated above. Next, as should be done whenever this file is updated, instruct Composer to dump a new list of all the files that should be autoloaded.
1
composer dump-autoload -o
Tip: When running the dump-autoload command, always pass the -o (optimize) flag.
58
phpunit There was 1 failure: 1) PracticeTest::testBuildsAnchorTag Failed asserting that null matches expected '<a href='http://:/dogs/1'>Show\ Dog</a>'.
Aha; so it seems that the test expected the anchor tag string, however, instead, only null was returned. Lets slime it as the first step.
1 2 3 4 5 6
Slime
Definition: Slime is nothing more than a bit of jargon that refers to returning a dummy value for the sole purpose of making a test pass. More traditionally, this is referred to as faking it. Feel free to use these terms interchangeably.
The principles of test-driven development dictate that tiny steps should be taken when coding. In other words, what is the simplest way to make a test pass? Initially, sliming it just might be the answer. Though hard-coding a returned value might seem foolish to you, the ultimate goal is to ensure that your code never becomes more complicated than is necessary to make the tests pass.
Generalize
Clearly, sliming is temporary. The key is to determine when its appropriate to generalize.
59
Definition: Think of the term, generalize, as a way to specify when its necessary to remove slime, in favor of real production code.
Perhaps an example is in order. Imagine a basic add function. A first assertion might be to ensure that, if you pass two and two, then the returned value from the function will, in fact, be four. What is the easiest possible solution to this assertion? Anyone, anyone? The solution is to slime it, and hardcode the returned value, like so:
1 2 3
Once you write a second test - perhaps, passing three and three - a hard-coded value will no longer suffice, at which point generalizing will become a necessity. See how that works? Calculating (excuse the pun) when to generalize is something that each developer must determine on their own.
60
Did You Know? Laravel, itself, offers a helpers file. In addition to the helpful url() function used above (simply a convenience that maps to URL::to(), it offers a number of particularly useful array operation commands.
Lets take things one step further. It might be nice if the link_to function offered a way to specify attributes for the anchor tag as well, such as a class or id. To make this a reality, first add a new test and specify the desired end result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?php class PracticeTest extends TestCase { public function testBuildsAnchorTag() { $actual = link_to('/dogs/1', 'Show Dog'); $expect = "<a href='http://:/dogs/1'>Show Dog</a>"; $this->assertEquals($expect, $actual); } public function testAppliesAttributesUsingArray() { $actual = link_to('/dogs/1', 'Show Dog', ['class' => 'button']); $expect = "<a href='http://:/dogs/1' class='button'>Show Dog</a>"; $this->assertEquals($expect, $actual); } }
As a basic rule of thumb, each test should represent but one path through your code.
Whats the next step? Run the tests and find out, fool.
61
phpunit 1) PracticeTest::testBuildsAnchorTag Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'<a href='http://:/dogs/1' class='button'>Show Dog</a>' +'<a href='http://:/dogs/1'>Show Dog</a>'
Got it? Lets make this new test pass. Well skip the sliming this time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<?php // app/helpers.php function link_to($url, $body, $parameters = null) { $url = url($url); $attributes = ''; // If the user specified any parameters, then // parse them and append to returned string if ($parameters) { foreach($parameters as $attribute => $value) { $attributes .= " {$attribute}='{$value}'"; } } return "<a href='{$url}'{$attributes}>{$body}</a>"; }
OK (2 tests, 2 assertions)
It passes! Now that we have a passing test, we can move on to any necessary refactoring, as allowed by the TDD pattern. In this case, I happen to know that Laravels HtmlBuilder class (you know it as the HTML facade) offers an attributes static method that will handle the parsing. Lets refrain from reinventing the wheel by instead leveraging that implementation.
62
function link_to($url, $body, $parameters = null) { $url = url($url); // If the user specified any parameters, then // parse them and append to returned string $attributes = $parameters ? HTML::attributes($parameters) : ''; return "<a href='{$url}'{$attributes}>{$body}</a>"; }
1) PracticeTest::testGeneratesAnchorTagAcceptsAttributesArray Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'<a href='http://:/dogs/1' class='button'>Show Dog</a>' +'<a href='http://:/dogs/1' class="button">Show Dog</a>'
Luckily, in this case, the HTML::attributes() method simply uses double quotes, when our test expected singles. Thats a non-issue and an easy fix: update the tests $expect variable, like so:
1
And that should be enough to give you a passing test! Congratulations! You just used test-driven development to write a helpful function that can be used within your views. Thats all TDD is. Dont complicate it any more than that. Once again, the process is: Write a test that defines how you want to interact with your code Watch it fail Write the necessary production code to make it pass Refactor
Important: This form of testing conforms more to the Chicago style of TDD. There are those who would argue that the url function that was triggered inside link_to should have been mocked; were testing link_to, not url. There absolutely is value to this approach, and youll learn more about it (London school of TDD) soon. But, for now, just as TDD requires small steps, so does the learning process.
63
Testing Classes
The most common introduction to unit testing generally revolves around the creation of a calculator. I know what youre thinking: I hate calculator testing tutorials! I get it, trust me; nobody builds basic calculators in real life. Nonetheless, theres a reason why theyre used so frequently (along with car examples). A calculator consists of basic logic that every one of us knows by heart. Two plus two will always equal four. By removing the barrier to entry, you can dedicate your entire focus to learning the assertions, rather than worrying about production code logic. Like I said, though, I feel your pain. Rest assured that well dig into lots of fancy stuff over the course of this book. But one step at a time, padawan! Unit testing a class is no different than simple functions: instantiate the class, call the desired method, and write an assertion. To finish up this chapter, lets use TDD to write the obligatory calculator class. Well tackle this in two sections: 1. What is the simplest way to handle basic arithmetic? 2. How can we leverage polymorphism to allow for a more extensible calculator? This section will be framework-agnostic, so, if working along, create an empty directory on your desktop, and add a CalculatorTest.php file with the first test.
1 2 3 4 5 6 7 8
<?php // CalculatorTest.php class CalculatorTest extends PHPUnit_Framework_TestCase { public function testInstance() { new Calculator; } }
Of course; create Calculator.php within the project root. Were not worried about folder organization for this simple demo.
64
Next, while the easiest way to include this class is to require it at the top like we did earlier, lets stay in the habit of embracing Composer for autoloading classes. Create a new composer.json file, and specify that Calculator.php should be autoloaded.
1 2 3 4 5 6 7
A simple composer install and composer dump should add this file to the autoloader. Now, pass the --bootstrap="vendor/autoload.php option when calling phpunit, or create a configuration file. To keep things simple, well stick with the former.
1
And were green! What do we require from this class next? Well, Id like it to keep track of the current result at all times; it should default to 0 upon instantiation. How might we represent that as a test?
1 2 3 4 5 6 7 8 9
<?php // CalculatorTest.php class CalculatorTest extends PHPUnit_Framework_TestCase { public function testResultDefaultsToZero() { $calc = new Calculator; $this->assertSame(0, $calc->getResult()); } }
Tip: When you require strict comparison, always opt for assertSame over assertEquals.
65
PHPUnit will inform us that the getResult method does not yet exist, so lets create it.
1 2 3 4 5 6 7 8
Why hardcode 0? Have you forgotten already? Were slimin it old school. Dont worry; well fix it soon. Until then, though, Id like to test an add method. When I call this method, the value specified should be added to a property that keeps a tally of the current total.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php // CalculatorTest.php class CalculatorTest extends PHPUnit_Framework_TestCase { public function testResultDefaultsToZero() { $calc = new Calculator; $this->assertSame(0, $calc->getResult()); } public function testAddsNumbers() { $calc = new Calculator; $calc->add(5); $this->assertEquals(5, $calc->getResult()); } }
To make this second test pass, well need to introduce a $result property to keep track of the current total. Well also want to remove the slime.
https://dl.dropboxusercontent.com/u/774859/Laravel-Decoded-Stuff/slimer.jpg
66
// Calculator.php class Calculator { protected $result = 0; public function add($num) { $this->result += $num; } public function getResult() { return $this->result; } }
Already, though, Im seeing a potential snag. What if you pass a non-numeric value to the add method, such as an array or string? In those situations, I want to ensure that an exception is thrown.
Tip: Think of exceptions as your way, as the developer, of saying, I take exception to this action.
To throw an exception, if following the rules of TDD, we cant write any production code until a failing test is written.
1 2 3 4 5 6 7 8 9 10
// CalculatorTest.php /** * @expectedException InvalidArgumentException */ public function testRequiresNumericValue() { $calc = new Calculator; $calc->add('five'); }
Above, were using doc-blocks to inform PHPUnit that, given the provided code, we expect an exception to be thrown. Without altering any production code, of course, no exception will be thrown, which will cause this test to fail.
67
// Calculator.php public function add($num) { if ( ! is_numeric($num)) throw new InvalidArgumentException; $this->result += $num; }
Excellent - that was easy! Next, it would be helpful if I could pass a variable number of arguments to the add method, such as add(2, 2, 5).
1 2 3 4 5 6 7 8 9 10 11 12
// CalculatorTest.php public function testAcceptsMultipleArgs() { $calc = new Calculator; $calc->add(1, 2, 3, 4); $this->assertEquals(10, $calc->getResult()); $this->assertNotEquals( 'Snoop Doggy Dogg and Dr. Dre is at the door', $calc->getResult() ); // jokes, jokes, jokes }
Is this process starting to make sense, now? Write a test that defines what you wish you could do, watch the test fail, and then write the necessary production code to make it pass. Im trying to drill this into your head, if you hadnt realized! Even better, its fun (at least for us). Heres the modified code (or at least a first draft) to allow for passing multiple numbers to add.
68
<?php // Calculator.php class Calculator { protected $result = 0; public function add() { foreach (func_get_args() as $num) { if ( ! is_numeric($num)) throw new InvalidArgumentException; $this->result += $num; } } public function getResult() { return $this->result; } }
This time, we filter through all of the passed arguments, and, as long as the value is numeric, we add it to the $result property. This will return the tests to green, but Im beginning to worry a bit. What about when we implement the subtract, multiply, and divide methods? This may result in a lot of repeated code, breaking the dont repeat yourself (DRY) pattern. For example, lets add the necessary functionality to subtract numbers. First, the test:
1 2 3 4 5 6 7 8 9
// CalculatorTest.php public function testSubtract() { $calc = new Calculator; $calc->subtract(4); $this->assertEquals(-4, $calc->getResult()); }
69
// Calculator.php public function subtract() { foreach(func_get_args() as $num) { if (! is_numeric($num)) throw new InvalidArgumentException; $this->result -= $num; } }
Tip: When you find yourself copying and pasting code from one method to another, this is a tell-tale sign that refactoring is required.
Lets take a break, while the tests are green, and DRY up both the tests and production code.
70
<?php // CalculatorTest.php class CalculatorTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->calc = new Calculator; } public function testResultDefaultsToZero() { $this->assertSame(0, $this->calc->getResult()); } /** * @expectedException InvalidArgumentException */ public function testRequiresNumericValue() { $this->calc->add('five'); } public function testAcceptsMultipleArgs() { $this->calc->add(1, 2, 3, 4); $this->assertEquals(10, $this->calc->getResult()); $this->assertNotEquals( 'Snoop Doggy Dogg and Dr. Dre is at the door', $this->calc->getResult() ); // jokes, jokes, jokes } public function testAddsNumbers() { $this->calc->add(5); $this->assertEquals(5, $this->calc->getResult()); } public function testSubtractsNumbers() {
71
<?php // Calculator.php class Calculator { protected $result = 0; public function getResult() { return $this->result; } public function add() { $this->calculateAll(func_get_args(), '+'); } public function subtract() { $this->calculateAll(func_get_args(), '-'); } protected function calculateAll(array $nums, $symbol) { foreach ($nums as $num) { $this->calculate($num, $symbol); } }
72
protected function calculate($num, $symbol) { if ( ! is_numeric($num)) throw new InvalidArgumentException; switch ($symbol) { case '+': $this->result += $num; break; case '-': $this->result -= $num; break; } } }
Though the code above does introduce a couple more methods, it lays the structure for further complexity in the class. Now, the calculate method handles all logic for performing the arithmetic.
Polymorphism
If that switch statement doesnt feel right to you, have a celebratory Fig Newton and pat yourself on the back. Just about any time that you use a switch statement, your class is secretly screaming for polymorphism. Of course, there are exceptions to every rule, but use this as a guideline. Too many conditionals are a cause for concern. Lets redesign. The problem with the current setup is that every time we want to add a new operation type (add, subtract, multiply), we must add another method to the class and update the switch statement. Very quickly, this class could become unwieldy. It would be better if the class could call a method that performs the arithmetic, without having to be concerned over how that operation is performed. This is where polymorphism comes into play. It looks like well need to design our calculator a bit differently. This is specifically why its important to think before you begin coding. Create an interface (our contract), Operation, that contains a single method.
73
interface Operation { /** * Perform the arithmetic * * @param integer $num * @param integer $current * @return integer */ public function run($num, $current); }
The reason why coding to an interface is good practice is because the Calculator class will be calling methods on injected objects. We need to be sure that this run() method is available. By requesting an implementation of the Operation class, we can have complete assurance that such a method will be available at run-time. Next, wed create a new class for each type of operation. Heres one for addition:
1 2 3 4 5 6
class Addition implements Operation { public function run($num, $current) { return $current + $num; } }
Notice how simple that is to read? The best part is that its a cinch to test. Thats the idea! Now that we have one implementation of the Operation interface, we next need to update the tests to reflect this design change.
1 2 3 4 5 6 7 8
public function testAddsNumbers() { $this->calc->setOperands(5); $this->calc->setOperation(new Addition); $result = $this->calc->calculate(); $this->assertEquals(5, $result); }
To make this test pass, the production code, also, needs to be adjusted, like so:
74
<?php class Calculator { protected $result = null; protected $operands = []; protected $operation; public function getResult() { return $this->result; } public function setOperands() { $this->operands = func_get_args(); } public function setOperation(Operation $operation) { $this->operation = $operation; } public function calculate() { foreach ($this->operands as $num) { if ( ! is_numeric($num)) throw new InvalidArgumentException; $this->result = $this->operation->run($num, $this->result); } return $this->result; } }
Admittedly, this is slightly more complex, but itll pay off in spades when it comes time to extend the functionality.
75
Extensibility
To multiply numbers as well, the Calculator doesnt even need to be touched. Simply create a new Multiplication class that implements the interface that we created earlier, and pass it through. Calculator will then call its method without knowing how it behaves. This is the beauty of polymorphism. Of course, we start with a test to verify when our new multiplication feature is finished.
1 2 3 4 5 6 7 8
public function testMultipliesNumbers() { $this->calc->setOperands(2, 3, 5); $this->calc->setOperation(new Multiplication); $result = $this->calc->calculate(); $this->assertEquals(30, $result); }
Only then, when we have a failing test, would we work on the production code. Right?
1 2 3 4 5 6 7 8 9 10
class Multiplication implements Operation { public function run($num, $current) { // If this is the first calculation, // then return the only operand if (is_null($current)) return $num; return $current * $num; } }
Mocks
If youve been watching closely, you might have noticed that we broke one of our rules. If unit testing requires isolation, were cheating when the calculate method calls those other classes. Its important to realize that there are two schools of thought on this issue (often labeled Chicago vs. London TDD, as noted earlier in this chapter). My personal recommendation is to push for as much isolation as possible. Weve been somewhat lax in this chapter, only because Im trying to ease you into the process of testing. We havent yet gotten to stubs and mocks. Having said that, if you want a quick demonstration for how you might test this class in true isolation, see below. Please note that this code leverages a popular PHP mocking framework, called Mockery. An entire chapter of the book is dedicated to it, so youll come to know it well!
76
public function testAddsNumbers() { // Mock all outside objects. // We're not interested in testing those. // They should have their own tests. $mock = Mockery::mock('Addition'); // All we care about is verifying that // the proper method was called. $mock->shouldReceive('run') ->once() ->with(5, 0) ->andReturn(5); $this->calc->setOperands(5); // Rather than new Addition, we // pass in the mock object $this->calc->setOperation($mock); // And then it's business per usual! $result = $this->calc->calculate(); $this->assertEquals(5, $result); }
Remember: this methodology is only effective if the class that has been mocked, too, has tests! If we can prove that the Addition class works perfectly, then theres absolutely no reason to test it twice.
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php class AdditionTest extends PHPUnit_Framework_TestCase { public function testFindsTheSumOfNumbers() { $addition = new Addition; $sum = $addition->run(5, 0); $this->assertEquals(5, $sum); } }
77
Or, another way of thinking about this is, if the Addition class does happen to break at some point, because we used mocks, the Calculator class will still return green. Thats what we want.
Tip: An added bonus to using mocks in situations such as this is that they allow you to drastically reduce the number of tests. When mocking the operation class, we no longer need tests in CalculatorTest for verifying addition, subtraction, multiplication, etc. Each of those implementations will have its own tests.
Project Complete
With just a bit of effort, we have a well-tested class for performing basic arithmetic. Youd certainly want to add more to this, but Ill leave that part in your hands as home-work. So what was this all for? Why dedicate so much of a unit testing chapter to understanding polymorphism? When building testable applications, I want you to break each class down to its core responsibility. An Addition class is responsibile for adding numbers; a Subtract class is responsible for subtracting. Not only does this approach allow for cleaner and more extensible code, but it also makes the process of testing significantly easier. Memorize this: The smaller the class, the easier it is to test. Hopefully, even though we used a generic calculator as the example, you were able to learn a variety of new techniques, including unit testing, the single responsibility principle, polymorphism, and a touch of Mockery. But, theres much more to learn. As Happy would say, weve only just begun.
Final Source
First, the CalculatorTest. Dont forget: once you learn more about mocks and stubs, the tests below can be improved, refactored, and reduced. Refer to the Mocks section earlier in this chapter for a teaser.
https://dl.dropboxusercontent.com/u/774859/Laravel-Decoded-Stuff/happy.png
78
<?php // CalculatorTest.php class CalculatorTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->calc = new Calculator; } public function testResultDefaultsToNull() { $this->assertNull($this->calc->getResult()); } public function testAddsNumbers() { $this->calc->setOperands(5); $this->calc->setOperation(new Addition); $result = $this->calc->calculate(); $this->assertEquals(5, $result); } /** * @expectedException InvalidArgumentException */ public function testRequiresNumericValue() { $this->calc->setOperands('five'); $this->calc->setOperation(new Addition); $this->calc->calculate(); } public function testAcceptsMultipleArgs() { $this->calc->setOperands(1, 2, 3, 4); $this->calc->setOperation(new Addition); $result = $this->calc->calculate(); $this->assertEquals(10, $result); $this->assertNotEquals( 'Snoop Doggy Dogg and Dr. Dre is at the door',
79
$result ); // jokes, jokes, jokes } // Notice how these next two tests are redundant, now that // we're using polymorphism. They're unnecessary. public function testSubtractsNumbers() { $this->calc->setOperands(4); $this->calc->setOperation(new Subtraction); $result = $this->calc->calculate(); $this->assertEquals(-4, $result); } public function testMultipliesNumbers() { $this->calc->setOperands(2, 3, 5); $this->calc->setOperation(new Multiplication); $result = $this->calc->calculate(); $this->assertEquals(30, $result); } }
And the Calculator class, itself. Please note that the various classes have been grouped for convenience. In your projects, each class should be contained within its own file. The only exception to this rule, in this authors opinion, is when you have a class that is only applicable within the context of another. This can be applied to exception classes and value objects.
1 2 3 4 5 6 7 8 9 10 11
<?php // Calculator.php interface Operation { public function run($num, $current); } class Addition implements Operation { public function run($num, $current) { return $current + $num; }
80
} class Multiplication implements Operation { public function run($num, $current) { if (is_null($current)) return $num; return $current * $num; } } class Subtraction implements Operation { public function run($num, $current) { return $current - $num; } } class Calculator { protected $result = null; protected $operands = []; protected $operation; public function getResult() { return $this->result; } public function setOperands() { $this->operands = func_get_args(); } public function setOperation(Operation $operation) { $this->operation = $operation; } public function calculate() {
81
foreach ($this->operands as $num) { if ( ! is_numeric($num)) throw new InvalidArgumentException; $this->result = $this->operation->run($num, $this->result); } return $this->result; } }
Summary
Plenty of unit testing introductions will overwhelm you with theory and jargon. But who does this serve? Its not dissimilar to teaching basic Math by beginning with Geometry - its simply not the right way to go about things. Im reminded of a quote from the movie, Contact. This was just the first step; in time youll take another. Yes, folks, the movie quotes will continue. This subject matter is just too dry not to sporadically throw in some geeky movie references.
Warning: Please note that this chapter assumes that you have a basic understanding of Git. If you dont, now is the best time to learn. A full understanding is vital for todays modern programmer.
Creating a Proposal
Before contributing to Laravel, its best to first create a Proposal that describes your intentions. More than anything, this is meant to protect you from potentially wasting hours writing new code, only to find that Taylor doesnt quite feel that it belongs in core. To submit a proposal, visit the issues section of the repository, and create a new thread with a descriptive title that begins with [Proposal]. Heres an example:
http://enva.to/laravel-6 https://github.com/laravel/framework/issues
82
83
In response, Taylor will either give you a thumbs up or down. If he responds favorably, then youre free to get to work!
Tip: Never feel badly if the owner of a project rejects or modifies your pull request. Ultimately, its their baby. As such, its vital that your contribution feels right to them. The important thing, though, is that youre helping, even if they ultimately apply their own stamp to the finished implementation.
For simple bug fixes, however, you may omit the proposal.
84
Tip: The standard installation of the Laravel framework wont include each packages PHPUnit tests. Always clone laravel/framework.
At this point, you should have both a tests and src directory. Remember when I noted that its a best practice for tests to mimic the folder structure of the files in which theyre associated? Well, that still holds true here.
Branching
As a basic rule of thumb, its a good idea to create a new Git branch for each bug fix or feature. This allows you to spend as much time needed, while leaving the master branch untouched. Most development teams will have their own naming conventions for branches. Here are two examples:
85
This single command will both create a new branch and check it out.
Coding Guidelines
An important component to successful pull requests is adherence to the projects coding guidelines. Always match the project owners style. Here are a few Laravel-specific examples:
1 2 3 4 5 6 7 8 9 10
All functions and control structure braces should be on their own line. The only point when this is not true is for the opening class brace. Also, take note of the space after foreach.
1 2 3 4 5 6 7 8 9 10 11
86
// WRONG <?php namespace Illuminate\Html; class Foo { } // CORRECT <?php namespace Illuminate\Html; class Foo { }
// WRONG return ['foo' => 'bar']; // CORRECT return array('foo' => 'bar');
Clearly, in a perfect world, the former syntax is preferred; however, Laravel still supports PHP 5.3. Consequently, any code that is 5.4 specific must be changed, unfortunately. There are other Taylor-specific guidelines, but youll learn these in due time. The most important thing is that you pay attention. Youre applying code to his project, not your own. So, in the same way that you might take off your shoes when entering someone elses home, show the same respect for projects on GitHub.
Hands On
Assuming that each of the prior steps have been followed, lets use TDD to add a bit of functionality.
Agenda
Wouldnt it be helpful if Laravels FormBuilder class offered a couple of additional methods for selectYear and selectMonth? Rather than dynamically creating a <select> element and all of the applicable <option>s, we could instead call a single method, and itll take care of the rest. How might we implement that sort of functionality? Well, wed start with a test, of course!
87
selectYear
The tests for the FormBuilder class are located within tests/Html/FormBuilderTest.php. Within this file, lets add a new test to describe the end goal.
1 2 3 4 5 6 7 8 9 10 11 12
// tests/Html/FormBuilderTest.php public function testFormSelectYear() { $select = $this->formBuilder->selectYear('year', 2000, 2001); $this->assertEquals( '<select name="year"><option value="2000">2000</option><option valu\ e="2001">2001</option></select>', $select ); }
Pretty simple, right? When the selectYear method is called (using its respective facade, this would be Form::selectYear()), and both a name and range are passed as arguments, we expect the provided HTML fragment in return.
Tip: Technically, if following the rules of TDD, we should only have written enough of the test to make it fail. However, this doesnt translate overly well to a book. As such, well take a couple of short-cuts here and there.
phpunit tests/Html
well be informed that the selectYear method does not exist. As should be familiar to you by now, the name of the game is tiny steps. 1. Run the tests. Watch it fail. 2. Write just enough code to make the test pass, or change the message. 3. Rinse and repeat. The FormBuilder class is located in src/Illuminate/Html/FormBuilder.php. Below the select method, add the new one:
88
1) FormBuilderTest::testFormSelectYear Failed asserting that an array contains '<select name="year"><option value=\ "2000">2000</option><option value="2001">2001</option></select>'.
public function selectYear($name, $begin, $end, $selected = null, $options \ = array()) { $range = range($begin, $end); // We want the value for each option to // be the same as the text content $range = array_combine($range, $range); return $this->select($name, $range, $selected, $options); }
89
It passes! Because the FormBuilder class already offers a select method, theres no need to reinvent the wheel. We only need to create the applicable array, and then pass it to select(). However, notice that this code isnt specific to years. It could be applied to any range. With that in mind, perhaps it would be better to extract this code out to its own method, selectRange, and then simply call it from selectYear. Because we have passing tests, were free to refactor. Heres the new implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
public function selectYear() { return call_user_func_array( array($this, 'selectRange'), func_get_args() ); } public function selectRange($name, $begin, $end, $selected = null, $options\ = array()) { $range = range($begin, $end); // We want the value for each option to // be the same as the text content $range = array_combine($range, $range); return $this->select($name, $range, $selected, $options); }
Well know if our refactoring was successful, because, if it was wasnt, wed see red! Luckily, we dont. We should add a couple more tests - just to verify that applying a selected value and options work as expected.
90
public function testFormSelectYear() { $select1 = $this->formBuilder->selectYear('year', 2000, 2001); $select2 = $this->formBuilder->selectYear('year', 2000, 2001, null, arr\ ay('id' => 'foo')); $select3 = $this->formBuilder->selectYear('year', 2000, 2001, '2000'); $this->assertEquals('<select name="year"><option value="2000">2000</opt\ ion><option value="2001">2001</option></select>', $select1); $this->assertEquals('<select id="foo" name="year"><option value="2000">\ 2000</option><option value="2001">2001</option></select>', $select2); $this->assertEquals('<select name="year"><option value="2000" selected=\ "selected">2000</option><option value="2001">2001</option></select>', $sele\ ct3); }
Note that were grouping these three assertions into a single method primarily because were following the basic format of the FormBuilderTest class. In my own applications, Id be more apt to split these into three unique methods. The tests return green, so the functionality must have been implemented correctly!
selectMonth
Now that both selectYear and selectRange have been implemented, adding selectMonth shouldnt be too difficult. We begin with a few tests (well move more quickly this time):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public function testFormSelectMonth() { $month1 = $this->formBuilder->selectMonth('month'); $month2 = $this->formBuilder->selectMonth('month', '1'); $month3 = $this->formBuilder->selectMonth('month', null, array('id' => \ 'foo')); $this->assertContains('<select name="month"><option value="1">January</\ option>', $month1); $this->assertContains('<select name="month"><option value="1" selected=\ "selected">January</option>', $month2); $this->assertContains('<select id="foo" name="month"><option value="1">\ January</option>', $month3); }
91
public function selectMonth($name, $selected = null, $options = array()) { $months = []; foreach (range(1, 12) as $month) { $months[$month] = strftime('%B', mktime(0, 0, 0, $month)); } return $this->select($name, $months, $selected, $options); }
Tip: The strftime function is particularly helpful, as its locale aware; the months value can reflect the developers preferred spoken language.
Summary
Newcomers to GitHub often feel hurt upon having their pull requests rejected. Thick skin is the key. Just because your implementation or feature doesnt mesh well with the projects creator doesnt mean that youre a bad coder, or shouldnt continue contributing. It simply means that the creator had a different vision for that particular piece of functionality. Its their baby; not yours. Guess what? Its time to write your first proposal!
What to Test
Ive drilled this into your head multiple times now. The basic rule is that everything that has an opportunity to break should be tested. More directly, plan on, at the very least, testing: Validations Scopes Accessors and Mutators Associations/Relationships Custom Methods
This can be particularly helpful when a value needs to be manipulated in some way. Here are a couple of examples. 92
93
<?php class Cat extends Eloquent { public function setAgeAttribute($age) { $this->attributes['age'] = $age * 7; } }
Now, when age is set on the model, it will first be filtered through this method, which multiplies the age by seven (the ratio of cat to human years). In effect:
1 2 3 4
public function testAutomaticallyCompensatesForCatYears() { $cat = new Cat; $cat->age = 6; $this->assertEquals(42, $cat->age); // true }
94
<?php class User extends Eloquent { public function setPasswordAttribute($password) { $this->attributes['password'] = Hash::make($password); } }
Testing this method is only slightly more tricky. Because the mutator calls Hash::make, if we want to test in isolation, then that class needs to be mocked. Luckily, because its a facade, doing so is trivial.
1 2 3 4 5 6 7 8 9
public function testHashesPasswordWhenSet() { Hash::shouldReceive('make')->once()->andReturn('hashed'); $author = new User; $author->password = 'foo'; $this->assertEquals('hashed', $author->password); }
This test uses a mock as a way of saying: I expect the make method on the Hash class to be triggered one time. When this occurs, rather than calling the original method, Ill just return the string, hashed. This technique offers three advantages: 1. We continue testing in complete isolation from external objects. 2. If the mutator does not call Hash::make, the expectation will fail, and well be notified immediately upon running phpunit. 3. Testing Hash::make can be tricky, because its return value will be somewhat random. Though you could, perhaps, ensure that the length of the mutated password is sixty characters (matching a hashs length), why bother when you can instead mock the dependency entirely?
Custom Methods
Of course, there will also be various custom methods within your model that should be tested. Approach these in the same way that you would any other test. For example, lets assume that an Article model should offer a way to retrieve meta information in readable form. This time, lets write the test first.
95
<?php class ArticleTest extends TestCase { public function testGetsReadableMetaData() { $article = new Article; $article->title = 'My First Article'; $article->author = 'Perd Hapley'; $this->assertEquals( '"My First Article" was written by Perd Hapley.', $article->meta() ); } }
So, when a meta method is called for the given model, we expect the string, My First Article was written by Perd Hapley. to be returned. The production code to make this test pass simply needs to fetch a couple attribute values and format them as a sentence. Rather than dealing with a bunch of concatenation, lets stick with PHPs native sprintf function.
1 2 3 4 5 6 7 8 9 10 11 12
<?php class Article extends Eloquent { public function meta() { return sprintf( '"%s" was written by %s.', $this->title, $this->author ); } }
Testing is easy!
96
<?php class User extends Eloquent { public function getOldest() { return $this->orderBy('age', 'desc')->first(); } }
How would you test this? Well, I see three possibilities: 1. Dont bother testing it at all. 2. Write an integration test. Insert a couple test rows into the DB, call the method, and verify that the correct row is returned. 3. Partially mock the User class, and verify that the orderBy method is called. As with many things, opinions vary, however, my recommendation is to write an integration test (which will require a test database). Because this entire method is simply a wrapper for a query builder, it doesnt make too much sense to partially mock the orderBy method. Why? Well that would be the only thing that the test does. Is that helpful? Not really. The best choice is to: 1. Insert a couple of test records into the DB 2. Call the method (without mocking) 3. Verify that the correct record was returned Heres an example, using the Laravel Test Helpers package (youll learn more about this soon), that does this very thing:
1 2 3 4 5 6 7 8 9 10 11 12
public function testGetsOldestUser() { // Arrange: Insert two test rows into a test DB Factory::create('User', ['age' => 20]); Factory::create('User', ['age' => 30]); // Act: call the method $oldest = (new User)->getOldest(); // Assert $this->assertEquals(30, $oldest->age); }
https://gist.github.com/JeffreyWay/5674014 https://github.com/JeffreyWay/Laravel-Test-Helpers
97
Validations
Another relatively simple way to ease into the process of testing models is to test validations. As an example, to create a new author, a name field or property must be present. Otherwise, its invalid, right? How might we verify that in a test?
1 2 3 4 5 6 7 8
Notice how the name of the test describes our desired behavior for the model. Running phpunit will, of course, fail, alerting you that an Author model does not exist.
1
Tip: When using my generators tool, I prefer to create bash aliases, such as g:m to generate a model, g:mig for a migration, etc. As such, to create a new Author model, I would simply run g:m author from the Terminal.
Should you run the tests again, youll be notified that a validate method does not exist on the model. Lets add this to a BaseModel class that all models can inherit from. But, before continuing on with this current test, we need to ensure that this new validate method works correctly. Because we havent yet fully gone over mocks and stubs, dont worry if some of the following code appears foreign to you.
https://github.com/JeffreyWay/Laravel-4-Generators
98
<?php // app/tests/BaseModelTest.php class BaseModelTest extends TestCase { protected $model; public function setUp() { parent::setUp(); $this->model = $model = new BaseModel; $model::$rules = ['title' => 'required']; } public function testReturnsTrueIfValidationPasses() { Validator::shouldReceive('make')->once()->andReturn( Mockery::mock(['passes' => true]) ); $this->model->title = 'Foo Title'; $result = $this->model->validate(); $this->assertTrue($result); } public function testSetsErrorsOnObjectIfValidationFails() { Validator::shouldReceive('make')->once()->andReturn( Mockery::mock(['passes' => false, 'messages' => 'messages']) ); $result = $this->model->validate(); $this->assertFalse($result); $this->assertEquals('messages', $this->model->errors); } }
Again, notice how the names of the tests describe how the code should behave. This is preferred over testValidate. These two tests account for both paths through the method: successful and failed validation. Because were testing in isolation, its paramount that we refrain from triggering Laravels native Validator class. Or, in other words, we dont need proof that that class works
99
correctly. Taylor has already tested it. As such, we can mock it. Youll learn all of this in the Mockeryspecific chapter shortly. Heres the associated code to make those two tests pass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<?php // app/models/BaseModel.php class BaseModel extends Eloquent { public $errors; public function validate() { $v = Validator::make($this->attributes, static::$rules); if ($v->passes()) return true; $this->errors = $v->messages(); return false; } }
This validate method consists of fairly boilerplate code to validate the current set of attributes against a static rules property that should exist on each model. Now that a validate method is available to all models, we can return to the former test. Here it is again for reference.
1 2 3 4 5 6 7 8
And its related production code, which now inherits from the newly added BaseModel.
100
When making use of the generic assertTrue and assertFalse methods, always provide a custom message to avoid meaningless failures, like Failed asserting that true is false.
1
Although we expected the validation to fail, because no rules have been set, it did not. To fix that, update the $rules property on the Author model, as needed.
1 2 3 4 5 6 7
<?php // app/models/Author.php class Author extends BaseModel { public static $rules = [ 'name' => 'required' ]; }
This should return us to green! Lets try a couple more. An email address should be required, too.
1 2 3 4 5 6 7 8 9 10 11 12
// app/tests/models/AuthorTest.php public function testIsInvalidWithoutAValidEmail() { // Set fixture $author = new Author; $author->name = 'Joe'; $author->email = 'foo'; $this->assertFalse($author->validate(), 'Expected validation to fail.')\ ; }
Once again, were back to a failing test. Not only should an email address be set, but it should adhere to the format of a real email address.
101
<?php // app/models/Author.php class Author extends BaseModel { public static $rules = [ 'name' => 'required', 'email' => 'required|email' ]; }
Tip: Refer to the Laravel documentation for a full list of available validation rules.
If you think about it, a valid email is not enough; we should also ensure that its a unique one. We dont want multiple authors with the same email address. Write a test (which touches the database once) to verify that this never happens.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// app/tests/models/AuthorTest.php public function testIsInvalidWithoutUniqueEmail() { // Set fixture $author = new Author; $author->name = 'Joe'; $author->email = '[email protected]'; $author->save(); // Now, try to insert a new author // with the same email. We don't want that. $author = new Author; $author->name = 'Frank'; $author->email = '[email protected]'; // Ensure that validation fails, because there already is // an author with that email registered. $this->assertFalse($author->validate(), 'Expected validation to fail.')\ ; }
http://four.laravel.com/docs/validation#available-validation-rules
102
Fixtures: Think of a fixture as a dummy record that can be inserted into a table for the purposes of testing.
Laravel offers a unique:TABLENAME validation rule that will serve our needs nicely here.
1 2 3 4 5 6 7 8
<?php // app/models/Author.php class Author extends BaseModel { public static $rules = [ 'name' => 'required', 'email' => 'required|email|unique:authors' ]; }
OK (3 tests, 3 assertions)
Warning: One glaring problem with this approach is that, as you add additional fields, it can lead to false positives in your tests. Its better to begin with a complete model, and then update individual fields, as needed. More on that later in this chapter.
Helpers
Because, as you might imagine, testing validations is so common, it makes sense to abstract this away to a custom assertion, such as assertValid and assertNotValid. If working with PHP 5.4 or higher, traits are a helpful choice for storing these sorts of mixins.
103
<?php // app/tests/helpers/ModelHelpers.php trait ModelHelpers { public function assertValid($model) { $this->assertTrue( $model->validate(), 'Model did not pass validation.' ); } public function assertNotValid($model) { $this->assertFalse( $model->validate(), 'Did not expect model to pass validation.' ); } }
Weve introduced a new app/tests/helpers directory, so dont forget to update composer, and then composer dump-autoload.
1 2 3 4 5 6 7 8 9
The wonderful thing about traits is that they can easily be imported into existing classes, via the use keyword. To pull these two mixins into AuthorTest, its as easy as doing:
104
Now, this more readable assertion syntax may be used for testing validations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// app/tests/models/AuthorTest.php public function testIsInvalidWithoutAName() { $author = new Author; $this->assertNotValid($author); } public function testIsInvalidWithoutAValidEmail() { $author = new Author; $author->name = 'Joe'; $this->assertNotValid($author); }
Factories
Up until this point, weve resorted to the somewhat cumbersome task of manually building test objects.
1 2 3
Surely, there must an easier way. Ideally, we could leverage a factory, which will handle the process of dynamically building the attributes for a model. Further, it should follow the clean Laravel style, such as:
1
$author = Factory::author();
Or, to create an object with all of the necessary fields, and insert it into the DB:
105
Factory::create('author');
Finally, we also need to have the option of overriding one or more fields default value.
1
Though building a tool like this from scratch is beyond the scope of this book, Ive already done the work for you! Its available on Packagist. Update your composer.json file as detailed on the Packagist page, pull in the dependencies, and, now, the tests may be updated to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
<?php // app/tests/models/AuthorTest.php use Way\Tests\Factory; class AuthorTest extends TestCase { use ModelHelpers; public function testIsInvalidWithoutAName() { $author = Factory::author(['name' => null]); $this->assertNotValid($author); } public function testIsInvalidWithoutAValidEmail() { $author = Factory::author(['email' => 'foo']); $this->assertNotValid($author); } public function testIsInvalidWithoutUniqueEmail() { $author = Factory::create('author', ['email' => '[email protected]'])\ ; // Now try to insert a new author with the same email. $author = Factory::author(['email' => '[email protected]']); // Let's make sure that it fails.
https://packagist.org/packages/way/laravel-test-helpers
106
$this->assertNotValid($author); } }
Much cleaner!
Factories
Ever found yourself repeatedly creating test models over and over?
1 2 3
This can very quickly flood your test classes. Instead, use a factory!
1 2 3 4 5 6 7 8 9 10 11
<?php use Way\Tests\Factory; class UserTest extends TestCase { public function testBasicExample() { $user = Factory::attributesFor('User'); } }
Now, the $user variable will be equal to random data that fits the data types for each field. Something like:
107
.array(6) { 'id' => NULL 'name' => string(3) "Kim" 'email' => string(15) "[email protected]" 'age' => int(26) 'created_at' => string(19) "2013-05-01 02:21:49" 'updated_at' => string(19) "2013-05-01 02:21:49" }
Overrides
There will be times, though, when you need to specify values for some fields. This can be particularly helpful for validation, where, say, a model should be invalid, unless an email is provided.
1
Any fields specified in the second argument will override the random defaults.
Models
The static attributesFor method is great for fetching an array of attributes. If you want the full Laravel collection, then you have two options:
1
$user = Factory::user();
This technique uses callStatic to allow for the readable syntax shown above. This works, if the model is in the global namespace, but if its not, youll want to use the make method.
1
$user = Factory::make('Models\User');
If you happen to be working with a real test database, you can also use the create method, which will instantiate the model, fill it with dummy data, and save it to the database.
108
$user = Factory::create('User');
Test Helpers
This package also includes a growing list of test helpers.
<?php use Way\Tests\Factory; class UserTest extends TestCase { use Way\Tests\ModelHelpers; public function testIsInvalidWithoutName() { $user = Factory::user(['name' => null]); $this->assertNotValid($user); } }
All model test helpers are stored as traits. This makes them super easy to import into our test class. Simply add use Way\Tests\ModelHelpers; to the top of the class, and you should be good to go. In the example above, we are asserting that a User model should be invalid, unless its name field is not empty. Currently, the assertion will look for a validate method on the model.
Asserting Relationships
There are also various assertions for Laravel relationships. Lets assert that a User model has many Posts.
109
And now were back to green. Currently, you can assert: assertBelongsTo assertHasOne assertHasMany
Summary
Arguments can be made for not testing controllers, where not a huge amount of logic is being performed, but, when it comes to models, its vital that you not cut any corners.
Mocking Decoded
A mock object is nothing more than a bit of test jargon that refers to simulating the behavior of real objects. In simpler terms, often, when testing, you wont want to execute a particular method. Instead, you simply need to ensure that it was, in fact, called. Perhaps an example is in order. Imagine that your code triggers a method that will log a bit of data to a file. When testing this logic, you certainly dont want to physically touch the file system. This has the potential to drastically decrease the speed of your tests. In these situations, its best to mock your file system class, and, rather than manually read the file to prove that it was updated, merely ensure that the applicable method on the class was, in fact, called. This is mocking! Theres nothing more to it than that; simulate the behavior of objects. Remember: jargon is just jargon. Never allow an initially confusing piece of terminology to deter you from learning a new skill. Particularly as your development process matures - including embracing the single responsibility principle and leveraging dependency injection - a familiarity with mocking will quickly become essential.
110
111
simply a dummy set of data that can be passed around to meet certain criteria. As youll find, there are multiple ways to fake data, each of which has its advantages, dependent upon the scenario. As a group, they are referred to as test doubles.
. The most popular testing library for PHP, PHPUnit, ships with its own API for mocking objects; however, unfortunately, it can prove cumbersome to work with. As youre surely aware, the more difficult testing is, the more likely it is that the developer simply (and sadly) wont. Luckily, a variety of third-party solutions are available through Packagist (Composers package repository), which allow for increased readability, and, more importantly, writeability. Among these solutions - and most notable of the set - is Mockery, a framework-agnostic mock object framework. Designed as a drop-in alternative for those who are overwhelmed by PHPUnits mocking verbosity, Mockery is a simple, but powerful utility. As youll surely find, in fact, its the industry standard for modern PHP development.
112
Installation
Like most modern PHP tools, Mockery may be installed with Composer.
Like most PHP tools these days, the recommended method to install Mockery is through Composer (though its available through Pear too). Wait, whats this Composer thing? Its the PHP communitys preferred tool for dependency management. It provides an easy way to declare a projects dependencies, and pull them in with a single command. As a modern PHP developer, its vital that you have a basic understanding of what Composer is, and how to use it. If working along, for learning purposes, add a new composer.json file to an empty project and append:
http://getcomposer.org
113
This bit of JSON specifies that, for development, your application requires the Mockery library. From the command-line, a composer install --dev will pull in the package.
1 2 3 4 5 6 7 8
$ composer install --dev Loading composer repositories with package information Installing dependencies (including require-dev) - Installing mockery/mockery (dev-master 5a71299) Cloning 5a712994e1e3ee604b0d355d1af342172c6f475f Writing lock file Generating autoload files
As an added bonus, Composer ships with its own autoloader for free! Either specify a classmap of directories and composer dump-autoload, or follow the PSR-0 standard and adjust your directory structure to match. Refer to Nettuts+ to learn more. If youre still manually requiring countless files in each PHP file, well, you just might be doing it wrong.
The Dilemma
Before we can implement a solution, its best to first review the problem. Imagine that you need to implement a system for handling the process of generating content and writing it to a file. Perhaps the generator compiles various data, either from local file stubs, or a web service, and then that data is written to the file system. If following the single responsibility principle - which dictates that each class should be responsible for exactly one thing - then it stands to reason that we should split this logic into two classes: one for generating the necessary content, and another for physically writing the data to a file. A Generator and File class, respectively, should do the trick. Tip: Why not use file_put_contents directly from the Generator class? Well, ask yourself: How could I test this? There are techniques, such as monkey patching, which can allow you to overload these sorts of things, but, as a best practice, its better to instead wrap such functionality up, so that it may easily be mocked with tools, like Mockery!
http://net.tutsplus.com/tutorials/php/psr-huh/
114
Heres a basic structure (with a healthy dose of pseudo code) for our Generator class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<?php // src/Generator.php class Generator { protected $file; public function __construct(File $file) { $this->file = $file; } protected function getContent() { // simplified for demo return 'foo bar'; } public function fire() { $content = $this->getContent(); $this->file->put('foo.txt', $content); } }
Dependency Injection
This code leverages what we refer to as dependency injection. Once again, this is simply developer jargon for injecting a classs dependencies through its constructor method, rather than hard-coding them. Why is this beneficial? Because, otherwise, we wouldnt be able to mock the File class! Sure, we could mock the File class, but if its instantiation is hard-coded into the class that were testing, theres no easy way to replace that instance with the mocked version.
1 2 3 4 5
115
The best way to build testable applications is to approach each new method call with the question, How might I test this? While there are tricks for getting around this hard-coding, doing so is widely considered to be a bad practice. Instead, always inject a classs dependencies through the constructor, or via setter injection. Setter injection is more or less identical to constructor injection. The principle is exactly the same; the only difference is that, rather injecting the classs dependencies through its constructor method, theyre instead done so through a setter method, like so:
1 2 3 4
A common criticism of dependency injection is that it introduces additional complexity into an application, all for the sake of making it more testable. Though the complexity argument is debatable in this authors opinion, if you should prefer, you can allow for dependency injection, while still specifying fallback defaults. Heres an example:
1 2 3 4 5 6
class Generator { public function __construct(File $file = null) { $this->file = $file ?: new File; } }
Now, if an instance of File is passed through to the constructor, that object will be used in the class. On the other hand, if nothing is passed, the Generator will fall back to manually instantiating the applicable class. This allows for such variations as:
1 2 3 4 5 6 7 8
# Class instantiates File new Generator; # Inject File new Generator(new File); # Inject a mock of File for testing new Generator($mockedFile);
Continuing on, for the purposes of this tutorial, the File class will be nothing more than a simple wrapper around PHPs file_put_contents function.
116
<?php // src/File.php class File { /** * Write data to a given file * * @param string $path * @param string $content * @return mixed */ public function put($path, $content) { return file_put_contents($path, $content); } }
Rather simple, eh? Lets write a test to see, first-hand, what the problem is.
1 2 3 4 5 6 7 8 9 10 11
<?php // tests/GeneratorTest.php class GeneratorTest extends PHPUnit_Framework_TestCase { public function testItWorks() { $file = new File; $generator = new Generator($file); $generator->fire(); } }
Please note that these examples assume that the necessary classes are being autoloaded with Composer. Your composer.json file optionally accepts an autoload object, where you may specify which directories or classes to autoload. No more messy require statements! If working along, running phpunit will return:
1
OK (1 test, 0 assertions)
Its green; that means we can move on to the next task, right? Well, not exactly. While its true that the code does, indeed, work, each time this test is run, a foo.txt file will be created on the
117
file system. What about when youve written dozens more tests? As you can imagine, very quickly, your tests speed of execution will stutter.
image
Though the tests pass, theyre incorrectly touching the filesystem. Still not convinced? If reduced testing speed wont sway you, then consider common sense. Think about it: were testing the Generator class; why do we have any interest in executing code from the File class? It should have its own tests! Why the heck would we double up?
The Solution
Hopefully, the previous section provided the perfect illustration for why mocking is essential. As was noted earlier, though we could make use of PHPUnit native API to serve our mocking requirements, its not overly enjoyable to work with. To illustrate this truth, heres an example for asserting that a mocked object should receive a method, getName and return John Doe.
118
While it gets the job done - asserting that a getName method is called once, and returns John Doe PHPUnits implementation is confusing and verbose. With Mockery, we can drastically improve its readability.
1 2 3 4 5 6 7
Notice how the latter example reads (and speaks) better. Continuing with the example from the previous Dilemma section, this time, within the GeneratorTest class, lets instead mock - or simulate the behavior of - the File class with Mockery. Heres the updated code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php class GeneratorTest extends PHPUnit_Framework_TestCase { public function tearDown() { Mockery::close(); } public function testItWorks() { $mockedFile = Mockery::mock('File'); $mockedFile->shouldReceive('put') ->with('foo.txt', 'foo bar') ->once();
119
Confused by the Mockery::close() reference within the tearDown method? This static call cleans up the Mockery container used by the current test, and run any verification tasks needed for your expectations. A class may be mocked using the readable Mockery::mock() method. Next, youll typically need to specify which methods on this mock object you expect to be called, along with any applicable arguments. This may be accomplished, via the shouldReceive(METHOD) and with(ARG) methods. In this case, when we call $generate->fire(), were asserting that it should call the put method on the File instance, and send it the path, foo.txt, and the data, foo bar.
1 2 3 4 5 6 7 8
Because were using dependency injection, its now a cinch to instead inject the mocked File object.
1
If we run the tests again, theyll still return green, however, the File class - and, consequently, the file system - will never be touched! Again, theres no need to touch File. It should have its own tests! Mocking for the win!
120
public function testSimpleMocks() { $user = Mockery::mock(['getFullName' => 'Jeffrey Way']); $user->getFullName(); // Jeffrey Way }
public function testDoesNotOverwriteFile() { $mockedFile = Mockery::mock('File'); $mockedFile->shouldReceive('exists') ->once() ->andReturn(true); $mockedFile->shouldReceive('put') ->never(); $generator = new Generator($mockedFile); $generator->fire(); }
This updated code now asserts that an exists method should be triggered on the mocked File class, and it should, for the purposes of this tests path, return true, signaling that the file already exists and shouldnt be overwritten. We next ensure that, in situations such as this, the put method on the File class is never triggered. With Mockery, this is easy, thanks to the never() expectation.
1 2
$mockedFile->shouldReceive('put') ->never();
121
Method exists() from File should be called exactly 1 times but called 0 times.
Aha; so the test expected that $this->file->exists() should be called, but that never happened. As a result, it failed. Lets fix it!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
<?php class Generator { protected $file; public function __construct(File $file) { $this->file = $file; } protected function getContent() { // simplified for demo return 'foo bar'; } public function fire() { $content = $this->getContent(); $file = 'foo.txt'; if (! $this->file->exists($file)) { $this->file->put($file, $content); } } }
Thats all there is to it! Not only have we followed a TDD (test-driven development) cycle, but the tests are back to green! Its important to remember that this style of testing is only effective if you do, in fact, test the dependencies of your class as well! Otherwise, though the tests may show green, for production, the code will break. Our demo this far has only ensured that Generator works as expected. Dont forget to test File as well!
122
Expectations
Lets dig a bit more deeply into Mockerys expectation declarations. Youre already familiar with shouldReceive. Be careful with this, though; its name is a bit misleading. When left on its own, it does not require that the method should be triggered; the default is zero or more times (zeroOrMoreTimes()). To assert that you require the method to be called once, or potentially more times, a handful of options are available:
1 2 3
There will be times when additional constraints are necessary. As demonstrated earlier, this can be particularly helpful when you need to ensure that a particular method is triggered with the necessary arguments. Its important to keep in mind that the expectation will only apply if a method is called with these exact arguments. Heres a few examples.
1 2 3
This can be extended even further to allow for the argument values to be dynamic in nature, as long as they meet certain criteria. Perhaps we only wish to ensure that a string is passed to a method:
1
$mock->shouldReceive('get')->with(Mockery::type('string'))->once();
Or, maybe the argument needs to match a regular expression. Lets assert that any file name that ends with .txt should be matched.
1 2 3
And as a final (but not limited to) example, lets allow for an array of acceptable values, using the anyOf matcher.
123
With this code, the expectation will only apply if the first argument to the get method is log.txt or cache.txt. Otherwise, a Mockery exception will be thrown when the tests are run.
1 2
Tip: Dont forget, you can always alias Mockery as m at the top of your class to make things a bit more succinct: use Mockery as m;. This allows for the more succinct, m::mock().
Lastly, we have a variety of options for specifying what the mocked method should do or return. Perhaps we only need it to return a boolean. Easy:
1 2 3
Partial Mocks
You may find that there are situations when you only need to mock a single method, rather than the entire object. Lets imagine, for the purposes of this example, that a method on your class references a custom global function (gasp) to fetch a value from a configuration file.
1 2 3 4 5 6 7 8 9 10
<?php class MyClass { public function getOption($option) { return config($option); } public function fire() {
124
While there are a few different techniques for mocking global functions, nonetheless, its best to avoid this method call altogether. This is precisely when partial mocks come into play.
1 2 3 4 5 6 7 8 9
Notice how weve placed the method to mock within brackets. Should you have multiple methods, simply separate them by a comma, like so:
1
With this technique, the remainder of the methods on the object will trigger and behave as they normally would. Keep in mind that you must always declare the behavior of your mocked methods, as weve done above. In this case, when getOption is called, rather than executing the code within it, we simply return 10000. An alternative option is to make use of passive partial mocks, which you can think of as setting a default state for the mock object: all methods defer to the main parent class, unless an expectation is specified. The previous code snippet may be rewritten as:
125
In this example, all methods on MyClass will behave as they normally would, excluding getOption, which will be mocked and return 10000.
Hamcrest
The Hamcrest library provides an additional set of matchers for defining expectations.
126
Once youve familiarized yourself with the Mockery API, its recommended that you also leverage the Hamcrest library, which provides an additional set of matchers for defining readable expectations. Like Mockery, it may be installed through Composer.
1 2 3 4
Once installed, you may use a more human-readable notation to define your tests. Below are a handful of examples, including slight variations that achieve the same end result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
<?php class HamCrestTest extends PHPUnit_Framework_TestCase { public function testHamcrestMatchers() { $name = 'Jeffrey'; $age = 28; $hobbies = ['coding', 'guitar', 'chess']; assertThat($name, is('Jeffrey')); assertThat($name, is(not('Joe'))); assertThat($age, is(greaterThan(20))); assertThat($age, greaterThan(20)); assertThat($age, is(integerValue())); assertThat(new Foo, is(anInstanceOf('Foo'))); assertThat($hobbies, is(arrayValue())); assertThat($hobbies, arrayValue()); assertThat($hobbies, hasKey('coding')); } }
Notice how Hamcrest allows you to write your assertions in as readable or terse a way as you desire. The use of the is() function is nothing more than syntactic sugar to aid in readability. Youll find that Mockery blends quite nicely with Hamcrest. For instance, with Mockery alone, to specify that a mocked method should be called with a single argument of type, string, you might write:
127
Hamcrest follows the resourceValue naming convention for matching the type of a value.
nullValue integerValue arrayValue
Summary
The biggest hurdle to using Mockery is, ironically, not the API, itself, but understanding why and when to use mocks in your testing. The key is to learn and respect the single responsibility principle in your coding workflow. Coined by Bob Martin, the SRP dictates that a class should have one, and only one, reason to change. In other words, a class shouldnt need to be updated in response to multiple, unrelated changes to your application, such as modifying business logic, or how output is formatted, or how data may be persisted. In its simplest form, just like a method, a class should do one thing. The File class manages file system interactions. A MysqlDb repository persists data. An Email class sends emails. Notice how, in none of these example was the word, and used. Once this is understood, testing becomes considerably easier. Dependency injection should be used for all operations that do not fall under the classs umbrella. When testing, focus on one class at a time, and mock all of its dependencies. Youre not interested in testing them anyways; they have their own tests! Though nothing prevents you from making use of PHPUnits native mocking implementation, why bother when Mockerys improved readability is only a composer update away?
Before moving forward, a word of caution: if you frequently come to the conclusion that a database call is necessary for your unit tests, then perhaps you should first consider redesigning. Is the class too tightly coupled to the database layer?
Test Databases
For the instances when its necessary to work with a fixture (an object with faked values), you certainly dont want to be using the production database. Luckily, Laravel can exist and function in multiple environments. For example, when triggering PHPUnit tests, the framework will automatically set the environment to testing. To specify custom configuration options on a per-environment basis, simply create a new folder within app/config/ that has the same name as the environment that its contents should correspond to. Notice that Laravel already includes an app/config/testing folder. As such, any configuration file that is placed here will take precendence over its production sibling. In translation, the contents of app/config/testing/database.php will override app/config/database.php. To specify a unique MySQL database for testing, you might write:
128
129
<?php // app/config/testing/database.php return array( 'connections' => array( 'mysql' => array( 'driver' => 'host' => 'database' => 'username' => 'password' => 'charset' => 'collation' => 'prefix' => ) ) );
Dont forget that, to save time, you can copy and paste the applicable bits from app/config/database.php. In this case, its only necessary to update the mysql connection to point to the new test database. Thats all it should take!
// app/tests/TestCase.php public function createApplication() { $unitTesting = true; $testEnvironment = 'testing'; return require __DIR__.'/../../bootstrap/start.php'; }
However, the same will not be true when running Artisan commands.
130
For instance, imagine that youve added a new migration to create an authors table with a few fields. If using my popular Laravel 4 Generators tool, this can be accomplished using a single command.
1 2
This command will generate the migration, as well as the necessary schema. However, upon migrating the database with php artisan migrate, youll find that this still executes the migration on the production database. Of course it does! How was Laravel supposed to know that you expected it to be applied to the testing DB? The --env option may be used to declare your desired environment.
1
Chances are, though, that youll want to refresh the migrations for each test. How might we accomplish that?
<?php // app/tests/models/AuthorTest.php class AuthorTest extends TestCase { public function setUp() { parent::setUp(); Artisan::call('migrate:refresh'); } }
Simply pass the name of an Artisan command as the first argument to Artisan::call(), and itll achieve the same end-result as:
https://github.com/JeffreyWay/Laravel-4-Generators
131
$this->seed();
Try It Out
Step 1: Create a book-testing database. Step 2: Install the migrations table (only necessary once):
1
This migration will, of course, be used for production, as well as testing. Step 4: Add a dummy test to app/tests/models/AuthorTest.php to ensure that the setUp() method runs at least once.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php # app/tests/models/AuthorTest.php class AuthorTest extends TestCase { public function setUp() { parent::setUp(); Artisan::call('migrate:refresh'); } public function testDummyToEnsureSetUpRuns() { $this->assertTrue(true); } }
Step 5: Run the tests: phpunit If executed successfully, youll see that the CreateMigrationsTable migration was executed, and the associated table was added to the test database. Success! You may now rest assured that each test will begin with a freshly reset and migrated database.
132
Databases in Memory
One popular technique for improving the performance of tests which touch the database is to opt for a Sqlite database in memory. Though benchmarks vary, this technique may still be one that you should consider. To instruct Laravel to use a database in memory, update app/config/testing/database.php to set a new default driver for testing.
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php // app/config/testing/database.php return array( 'default' => 'sqlite', 'connections' => array( 'sqlite' => array( 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ) ) );
Notice how, rather than setting the database key to a .sqlite file, instead, a DB in memory was specified. With this modification alone, when testing, Laravel will automatically set the environment to testing and reference the in-memory DB. When choosing this method, its vital that, before each test, you re-migrate and seed the database. Heres an example:
1 2 3 4 5 6 7
These two lines will construct and seed all necessary tables before firing each test.
133
Summary
When it comes to unit testing specifically, use the techniques outlined in this chapter as a last resort. First, make every effort to test in isolation. If doing so seems impossible, then its likely that your code should be decoupled. In a perfect world, test databases should only be used for outside-in tests.
Those of us with a moderate amount of experience with object-oriented programming and testing are conditioned to instantly squirm at the site of statics, like these. Sure, you can test a single static method; but, as soon as that method calls another method, youll quickly find yourself in a world of hurt when its time to test. Fortunately, Laravel - rather brilliantly, I might add - allows for this clean syntax, while still, behind the scenes, instantiating the applicable class, allowing for perfect testability. This makes for the best of both worlds, and is what we refer to as the Facade Pattern.
Facade: A facade is an object that provides a simplified interface to a larger body of code.
Other than readability, one of the significant advantages to this approach is that the underlying class can easily be swapped out with a mock for testing purposes. In translation, this means that you are free to use these facades within your code (without injecting them), while still allowing for complete testability.
Mockery
Every Laravel facade extends a parent Facade class that offers, among other things, a shouldReceive method. When called, Laravel will automatically swap out the registered instance with a mock, using Mockery. See for yourself:
134
135
// vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php /** * Initiate a mock expectation on the facade. * * @param dynamic * @return \Mockery\Expectation */ public static function shouldReceive() { $name = static::getFacadeAccessor(); if (static::isMock()) { $mock = static::$resolvedInstance[$name]; } else { static::$resolvedInstance[$name] = $mock = \Mockery::mock(static::g\ etMockableClass($name)); static::$app->instance($name, $mock); } return call_user_func_array(array($mock, 'shouldReceive'), func_get_arg\ s()); }
Dont worry if this code is difficult to interpret. You neednt understand every facet. Most importantly, though, recognize that the resolved instance is being swapped out with a mocked version.
1 2
Testing
For the purposes of simplicity, imagine that, when a particular URL is requested, a file will be created on the server. Its a silly example, but will illustrate this swapping functionality nicely. When unit
136
testing, we certainly dont want to physically create the file for each test. Its a better idea to mock the File class, like so:
1 2 3 4 5 6
This code declares that, when localhost:8000/foo is requested, we expect that the put method on the mocked version of the Filesystem class will be called. Assuming that no foo route currently exists, PHPUnit will, of course, fail:
1 2
1) ExampleTest::testCreatesFile Symfony\Component\HttpKernel\Exception\NotFoundHttpException:
Somehow, the test will now pass. But how is that possible? We declared that File::put() should be called once. Yet, even though the route closure is empty, the test still passes. Huh? As it turns out, youll fall into this trap often. Remember: because were leveraging Mockery, a tearDown method must always be declared in your test file. Among other things, this call will verify your expectations. To avoid repetition, you can place this tearDown method within the parent TestCase class that Laravel provides.
137
<?php // app/tests/FooTest.php class FooTest extends TestCase { public function tearDown() { Mockery::close(); } public function testCreatesFile() { File::shouldReceive('put')->once(); $this->call('GET', 'foo'); } }
1) ExampleTest::testCreatesFile Mockery\Exception\InvalidCountException: Method put() from Illuminate\Files\ ystem\Filesystem should be called exactly 1 times but called 0 times.
Here, we see Mockery hard at work. Lets make the test pass.
1 2 3 4 5 6
Were back to green! Admittedly, its a silly example, but, nonetheless, its really a very cool (and unique) feature of Laravel! Even though the routes callback calls File::put(), we can override the resolved instance of the Filesystem class and replace it with a mocked version - all by simply writing File::shouldReceive(). The end result is that the production code continues to be as readable as possible, while still allowing for the tests to run crazy fast. As an alternative to using shouldReceive, if you prefer, you can instead use the facades swap method, which will replace the underlying instance with the object that you pass in.
138
Mocking Events
Laravel 4 provides an easy way to fire and listen for custom events, via the Event class (also a facade). Using Tuts+ Premium as an example, it might make sense for the app to fire an event when a user cancels their subscription. This way, other parts of the application can respond by sending a cancellation email to the user, updating a report, clearing out a database recordany number of things. Heres what the destroy method on a users controller might look like.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public function destroy($id) { // Get the user who is canceling $user = $this->user->findOrFail($id); // Soft delete the user // To soft delete, set the $softDelete property // on the User model $user->delete(); // Make announcement, and send the $user object Event::fire('cancellation', ['user' => $user]); // Redirect to home page return Redirect::home(); }
When writing a functional test for this controller, we can simulate the fired event in the same way that we did with the File facade.
1 2 3 4 5 6 7 8 9 10
public function testDestroyUser() { Event::shouldReceive('fire') ->once() ->with(['cancellation', Mockery::any()]); // Perform any other necessary expectations $this->call('DELETE', '/users/1'); }
Even though we referenced the Event class explicitly, were still able to mock it with ease.
http://tutsplus.com
139
Summary
In this chapter, we reviewed Laravels unique use of facades, which, though confusing at first, allow for the readable static syntax that endeared all of us to Laravel in the first place. Even though it feels as if youre interacting with statics, these are merely illusions (or facades) that instantiate underlying classes, making for complete testability. Its the best of both worlds! The techniques outlined in this chapter are most useful for small to medium-sized projects. However, if you find yourself relying on mocking facades too much, it might be an indication that your code could be better organized.
Tip: The less work your controller does, the easier it is to test. This is true for all classes, and is one of the benefits of embracing the single responsibility principle.
140
141
<?php class PostsControllerTest extends TestCase { public function testIndex() { $this->client->request('GET', 'posts'); } }
Laravel leverages a handful of Symfonys components to ease the process of testing routes and views, including HttpKernel, DomCrawler, and BrowserKit. This is why its paramount that your PHPUnit tests inherit from, not PHPUnit_Framework_TestCase, but TestCase. Dont worry, Laravel still extends the former, but it helps setup the Laravel app for testing, as well as provides a variety of helper assertion methods that you are encouraged to use. More on that shortly. In the code snippet above, we make a GET request to /posts, or localhost:8000/posts. Assuming that this line is added to a fresh installation of Laravel, Symfony will throw a NotFoundHttpException. If working along, try it out by running phpunit from the command line.
142
In human-speak, this essentially translates to, Hey, I tried to call that route, but you dont have anything registered, fool! As you can imagine, this type of request is common enough to the point that it makes sense to provide a helper method, such as $this->call(). In fact, Laravel does that very thing! This means that the previous example can be refactored, like so:
1 2 3 4
Well, thats only partially true. Even though $this->call() and $this->client->request() will both call a particular route, the returned values will differ.
1 2 3 4
As a basic rule of thumb, if you intend to crawl the DOM - this bit of text should appear when the page is loaded - then use $this->client->request(), as it will return a Crawler instance. Otherwise, the call method, which will return a response object, will do just fine.
143
public function __call($method, $args) { if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) { return $this->call($method, $args[0]); } throw new BadMethodCallException; }
Now, youre free to write $this->get('posts') and achieve the exact same result as the previous two examples. As noted above, however, lets stick with the frameworks base functionality for simplicitys sake. To make the test pass, we only need to prepare the proper route.
1 2 3 4 5 6
This will trigger the index method on PostsController. Any applicable parameters may be passed as the third argument.
1
144
$posts variable to its associated view, right? That way, the view can filter through all posts, and
display them on the page. This is an important test to write! If its that common a task, then, once again, wouldnt it make sense for Laravel to provide a helper assertion to accomplish this very thing? Of course it would. And, of course, Laravel does!
Illuminate\Foundation\Testing\TestCase includes a number of methods that will drastically
reduce the amount of code needed to perform basic assertions. This list includes:
assertViewHas assertResponseOk assertRedirectedTo assertRedirectedToRoute assertRedirectedToAction assertSessionHas assertSessionHasErrors
The following example calls GET /posts and verifies that its views receives the variable, $posts.
1 2 3 4 5 6
Tip: When it comes to formatting, I prefer to provide a line break between a tests assertion and the code that prepares the stage.
assertViewHas is simply a bit of sugar that inspects the response object - which is returned from $this->call() - and verifies that the data associated with the view contains a posts variable.
When inspecting the response object, you have two core choices: $response->getOriginalContent(): Fetch the original content, or the returned View. Optionally, you may access the original property directly, rather than calling the getOriginalContent method. $response->getContent(): Fetch the evaluated output. If a View instance is returned from the route, then getContent() will be equal to the HTML output. This can be helpful for DOM verifications, such as the view must contain this string. Lets assume that the posts route consists of:
145
Should we run phpunit, it will squawk with a helpful next step message:
1 2
To make it green, we simply fetch the posts and pass it to the view.
1 2 3 4 5 6
Route::get('posts', function() { $posts = Post::all(); return View::make('posts.index', ['posts' => $posts]); });
One thing to keep in mind is that, as the code currently stands, it only ensures that the variable, $posts, is passed to the view. It doesnt inspect its value. The assertViewHas optionally accepts a second argument to verify the value of the variable, as well as its existence.
1 2 3 4 5 6
With this modified code, unless the view has a variable, $posts, that is equal to foo, the test will fail. In this situation, though, its likely that wed rather not specify a value, but instead declare that the value be an instance of Laravels Illuminate\Database\Eloquent\Collection class. How might we accomplish that? PHPUnit provides a helpful assertInstanceOf assertion to fill this very need!
146
public function testIndex() { $response = $this->call('GET', 'posts'); $this->assertViewHas('posts'); // getData() returns all vars attached to the response. $posts = $response->original->getData()['posts']; $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $pos\ ts); }
With this modification, weve declared that the controller must pass $posts - an instance of Illuminate\Database\Eloquent\Collection - to the view. Excellent.
Required Refactoring
Unfortunately, so far, weve structured the code in a way that makes it virtually impossible to test.
http://leanpub.com/laravel-testing-decoded
147
Route::get('posts', function() { // Ouch. We can't test this!! $posts = Post::all(); return View::make('posts.index') ->with('posts', $posts); });
This is precisely why its considered bad practice to nest Eloquent calls into your controllers. Dont confuse Laravels facades, which are testable and can be swapped out with mocks (Queue::shouldReceive()), with your Eloquent models. The solution is to inject the database layer into the controller through the constructor. This requires some refactoring.
Warning: Storing logic within route callbacks is useful for small projects and APIs, but they make testing incredibly difficult. For applications of any considerable size, use controllers.
Route::resource('posts', 'PostsController');
Now, rather than referencing the Post model directly, well inject it into the controllers constructor. Heres a condensed example that omits all restful methods, except the one that were currently interested in testing.
148
<?php class PostsController extends BaseController { protected $post; public function __construct(Post $post) { $this->post = $post; } public function index() { $posts = $this->post->all(); return View::make('posts.index') ->with('posts', $posts); } }
Tip:: Its a better idea to type-hint an interface, rather than reference the Eloquent model, itself. But, one thing at a time! Lets work up to that.
Dont worry about manually injecting the Post instance. Like magic, Laravel will do this automatically for you! This is referred to as automatic resolution. More on this shortly. This is a significantly better way to structure the code. Because the model is now injected, we have the ability to swap it out with a mocked version for testing. Heres an example of doing just that:
http://four.laravel.com/docs/ioc#automatic-resolution
149
<?php class PostsControllerTest extends TestCase { public function __construct() { // We have no interest in testing Eloquent $this->mock = Mockery::mock('Eloquent', 'Post'); } public function tearDown() { Mockery::close(); } public function testIndex() { $this->mock ->shouldReceive('all') ->once() ->andReturn('foo'); $this->app->instance('Post', $this->mock); $this->call('GET', 'posts'); $this->assertViewHas('posts'); } }
The key benfit to this restructuring is that, now, the database will never needlessly be hit. Instead, using Mockery, we merely verify that the all method is triggered on the model.
1 2 3
Unfortunately, if you choose to forego coding to an interface, and instead inject the Post model into the controller, a bit of trickery has to be used in order to get around Eloquents use of statics, which can clash with Mockery. This is why we hijack both the Post and Eloquent classes within the tests constructor, before the official versions have been loaded. This way, we have a clean slate to declare
150
any expectations. The downside, of course, is that we cant default to any existing methods, through the use of Mockery methods, like makePartial().
$this->app->instance('Post', $this->mock);
Think of this code as saying, Hey Laravel, when you need an instance of Post, I want you to use my mocked version. Because the app extends the Container we have access to all IoC methods directly off of it. Upon instantiation of the controller, Laravel leverages the power of PHP reflection to read the typehint and inject the dependency for you. Thats right; you dont have to write a single binding to allow for this; its automated!
Redirections
Another common expectation that youll find yourself writing is one that ensures that the user is redirected to the proper location, perhaps upon adding a new post to the database. How might we accomplish this?
1 2 3 4 5 6 7 8 9 10 11 12
public function testStore() { $this->mock ->shouldReceive('create') ->once(); $this->app->instance('Post', $this->mock); $this->call('POST', 'posts'); $this->assertRedirectedToRoute('posts.index'); }
Assuming that were following a restful flavor, to add a new post, wed POST to the collection, or posts (dont confuse the POST request method with the resource name, which just happens to have the same name).
151
$this->call('POST', 'posts');
You might prefer to also ensure that the $_POST super-global is passed to the create method. Even though we arent physically submitting a form, we can still allow for this, via the third parameter to the call method. Heres a modified test that uses Mockerys with() method to verify the arguments passed to the method referenced by shouldReceive.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public function testStore() { $input = ['title' => 'My Title']); $this->mock ->shouldReceive('create') ->once() ->with($input); $this->app->instance('Post', $this->mock); $this->call('POST', 'posts', $input); $this->assertRedirectedToRoute('posts.index'); }
Paths
One thing that we havent considered in this test is validation. There should be two separate paths through the store method, dependent upon whether the validation passes: 1. Redirect back to the Create Post form, and display the form validation errors. 2. Redirect to the collection, or the named route, posts.index. As a best practice, each test should represent but one path through your code. This first path will be for failed validation.
152
public function testStoreFails() { // Set stage for a failed validation $input = ['title' => '']; $this->app->instance('Post', $this->mock); $this->call('POST', 'posts', $input); // Failed validation should reload the create form $this->assertRedirectedToRoute('posts.create'); // The errors should be sent to the view $this->assertSessionHasErrors(['title']); }
The code snippet above explicitly declares which errors should exist. Alternatively, you may omit the argument to assertSessionHasErrors, in which case it will merely verify that a message bag has been flashed (in translation, your Redirection includes withErrors($errors)). Now for the test that handles successful validation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public function testStoreSuccess() { // Set stage for successful validation $input = ['title' => 'Foo Title']; $this->mock ->shouldReceive('create') ->once(); $this->app->instance('Post', $this->mock); $this->call('POST', 'posts', $input); // Should redirect to collection, with a success flash message $this->assertRedirectedToRoute('posts.index', ['flash']); }
The production code for these two tests might look like:
153
public function store() { $input = Input::all(); // We'll run validation in the controller for convenience // You should export this to the model, or a service $v = Validator::make($input, ['title' => 'required']); if ($v->fails()) { return Redirect::route('posts.create') ->withInput() ->withErrors($v->messages()); } $this->post->create($input); return Redirect::route('posts.index') ->with('flash', 'Your post has been created!'); }
Notice how the Validator is nested directly in the controller? Generally, Id recommend that you abstract this away to a service. That way, you can test your validation in isolation from any controllers or routes. Nonetheless, lets leave things as they are for simplicitys sake. One thing to keep in mind is that we arent mocking the Validator, though you certainly could do so. Because this class is a facade, it can easily be swapped out with a mocked version, via the Facades shouldReceive method, without us needing to worry about injecting an instance through the constructor. Win!
1 2 3
From time to time, youll find that a method that needs to be mocked should return an object, itself. Luckily, with Mockery, this is a piece of cake: we only need to create an anonymous mock, and pass an array, which signals the method name and response value, respectively. As such:
1
154
Repositories
To allow for optimal flexibility, rather than creating a direct link between your controller and an ORM, like Eloquent, its better to code to an interface. The considerable advantage to this approach is that, should you perhaps need to swap out Eloquent for, say, Mongo or Redis, doing so literally requires the modification of a single line. Even better, the controller doesnt ever need to be touched. Repositories represent the data access layer of your application. What might an interface for managing the database layer of a Post look like? This should get you started.
1 2 3 4 5 6 7 8 9 10 11
<?php namespace Repositories; interface PostRepositoryInterface { public function all(); public function find($id); public function create($input); }
This can certainly be extended, but weve added the bare minimum methods for the demo: all, find, and create. Notice that the repository interfaces are being stored within app/repositories. Because this folder is not autoloaded by default, we need to update the composer.json file for the application to reference it.
1 2 3 4 5 6
When a new class is added to this directory, dont forget to composer dump-autoload -o. The -o, (optimize) flag is optional, but should always be used, as a best practice. If you attempt to inject this interface into your controller, Laravel will snap at you. Go ahead; try it out and see. Heres the modified PostController, which has been updated to inject an interface, rather than the Post Eloquent model.
155
<?php use Repositories\PostRepositoryInterface as Post; class PostsController extends BaseController { protected $post; public function __construct(Post $post) { $this->post = $post; } public function index() { $posts = $this->post->all(); return View::make('posts.index', ['posts' => $posts]); } }
If you run the server and view the output, youll be met with the dreaded (but beautiful) Whoops error page, declaring that PostRepositoryInterface is not instantiable.
156
Not Instantiable?
If you think about it, of course the framework is squawking! Laravel is smart, but its not a mind reader. It needs to be told which implementation of the interface should be used within the controller. For now, lets add this binding to app/routes.php. Later, well instead make use of service providers to store this sort of logic.
1 2 3 4
Verbalize this function call as, Laravel, baby, when you need an instance of PostRepositoryInterface, I want you to use EloquentPostRepository.
app/repositories/EloquentPostRepository will simply be a wrapper around Eloquent that implements PostRepositoryInterface. This way, were not restricting the API (and every other
157
<?php namespace Repositories; use Repositories\PostRepositoryInterface; use Post; class EloquentPostRepository implements PostRepositoryInterface { public function all() { return Post::all(); } public function find($id) { return Post::find($id); } public function create($input) { return Post::create($input); } }
Some might argue that the Post model should be injected into this implementation for testability purposes. If you agree, simply inject it through the constructor, per usual. Thats all it should take! Refresh the browser, and things should be back to normal. Only, now, your application is far better structured, and the controller is no longer linked to Eloquent. Lets imagine that, a few months from now, your boss informs you that you need to swap Eloquent out with Redis. Well, because youve structured your application in this future-proof way, you only need to create the new app/repositories/RedisPostRepository implementation:
158
<?php namespace Repositories; use Repositories\PostRepositoryInterface; class RedisPostRepository implements PostRepositoryInterface { public function all() { // return all with Redis } public function find($id) { // return find one with Redis } public function create($input) { // return create with Redis } }
Instantly, youre now leveraging Redis in your controller. Notice how app/controllers/PostsController.php was never touched? Thats the beauty of it!
Structure
So far in this lesson, our organization has been a bit lacking. IoC bindings in the routes.php file? All repositories grouped together in one directory? Sure, that may work in the beginning, but, very quickly, itll become apparent that this doesnt scale. In the final section of this chapter, well PSR-ify our code, and leverage service providers to register any applicable bindings.
159
PSR-0 defines the mandatory requirements that must be adhered to for autoloader interoperability.
A PSR-0 loader may be registered with Composer, via the psr-0 object.
1 2 3 4 5
The syntax can be confusing at first. It certainly was for me. An easy way to decipher "Way": "app/lib/" is to think to yourself, The base folder for the Way namespace is located in app/lib. Of course, replace my last name with the name of your project. The directory structure to match this would be: app/ lib/ * Way/ Next, rather than grouping all repositories into a repositories directory, a more elegant approach might be to categorize them into multiple directories, like so: app/ lib/ * Way/ Storage/ Post/ PostRepositoryInterface.php EloquentPostRepository.php Its vital that we adhere to this naming and folder convention, if we want the autoloading to work as expected. The only remaining thing to do is update the namespaces for PostRepositoryInterface and EloquentPostRepository.
160
<?php namespace Way\Storage\Post; interface PostRepositoryInterface { public function all(); public function find($id); public function create($input); }
<?php namespace Way\Storage\Post; use Post; class EloquentPostRepository implements PostRepositoryInterface { public function all() { return Post::all(); } public function find($id) { return Post::find($id); } public function create($input) { return Post::create($input); } }
There we go; thats much cleaner. But what about those pesky bindings? The routes file may be a convenient place to experiment, but it makes little sense to store them there permanently. Instead, well use service providers.
161
Service providers are nothing more than bootstrap classes that can be used to do anything you wish: register a binding, hook into an event, import a routes file, etc.
<?php namespace Way\Storage; use Illuminate\Support\ServiceProvider; class StorageServiceProvider extends ServiceProvider { // Triggered automatically by Laravel public function register() { $this->app->bind( 'Way\Storage\Post\PostRepositoryInterface', 'Way\Storage\Post\EloquentPostRepository' ); } }
To make this file known to Laravel, you only need to include it in app/config/app.php, within the providers array.
1 2 3 4 5 6
162
public function testIndex() { $mock = Mockery::mock('Way\Storage\Post\PostRepositoryInterface'); $mock->shouldReceive('all')->once(); $this->app->instance('Way\Storage\Post\PostRepositoryInterface', $mock)\ ; $this->call('GET', 'posts'); $this->assertViewHas('posts'); }
However, we can improve this. It stands to reason that every method within PostsControllerTest will require a mocked version of the repository. As such, its better to extract some of this prep work into its own method, like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
public function setUp() { parent::setUp(); $this->mock = $this->mock('Way\Storage\Post\PostRepositoryInterface'); } public function mock($class) { $mock = Mockery::mock($class); $this->app->instance($class, $mock); return $mock; } public function testIndex() { $this->mock->shouldReceive('all')->once(); $this->call('GET', 'posts'); $this->assertViewHas('posts'); }
163
Now, if you want to be super-fly, and are willing to add a touch of test logic to your production code, you could even perform your mocking within the Eloquent model! This would allow for:
1
Post::shouldReceive('all')->once();
Behind the scenes, this would mock PostRepositoryInterface, and update the IoC binding. You cant get much more readable than that! Allowing for this syntax only requires you to update the Post model, or, better, a BaseModel that all of the Eloquent models extend. Heres an example of the former:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php class Post extends Eloquent { public static function shouldReceive() { $class = get_called_class(); $repo = "Way\\Storage\\{$class}\\{$class}RepositoryInterface"; $mock = Mockery::mock($repo); App::instance($repo, $mock); return call_user_func_array( [$mock, 'shouldReceive'], func_get_args() ); } }
If you can manage the inner Should I be embedding test logic into production code battle, youll find that this allows for significantly more readable tests.
1 2 3 4 5 6 7 8 9
164
public function testIndex() { Post::shouldReceive('all')->once(); $this->call('GET', 'posts'); $this->assertViewHas('posts'); } public function testStoreFails() { $input = ['title' => '']; $this->call('POST', 'posts', $input); $this->assertRedirectedToRoute('posts.create'); $this->assertSessionHasErrors(); } public function testStoreSuccess() { $input = ['title' => 'Foo Title']; Post::shouldReceive('create')->once(); $this->call('POST', 'posts', $input); $this->assertRedirectedToRoute('posts.index', ['flash']); } }
It feels good, doesnt it? Hopefully, this chapter hasnt been too overwhelming. The key is to learn how to organize your repositories in such a way to make them as easy as possible to mock and inject into your controllers. As a result of that effort, your tests will be lightning fast!
165
When requesting a URI with $this->client->request(), a Crawler object will be returned, which can then be used to filter the DOM and perform assertions.
public function testLogin() { $crawler = $this->client->request('GET', '/login'); $h1 = $crawler->filter('h1'); $this->assertEquals('Please Login', $h1->text()); }
Above, were asserting that, when /login is requested, we expect to see an <h1> tag that contains the text, Please Login.
Tip: The filter method may be used to filter the DOM, using the simple CSS selectors that were all familiar with. Alternatively, XPath may be used, but stick with the CSSSelector component, if you can. Its far more readable.
In the example above, the assertion will only pass if the first occurrence of an <h1> tag contains Please Login. If you need to be more flexible, the assertion could be rewritten, like so:
1 2
Basic Traversing
The DomCrawler component offers a handful of traversal methods that can be used for further filtering. Many of these are simply helpers that could also be accomplished by modifying the CSS selector directly. Nonetheless, they can be a helpful convenience.
Fetch By Position
1
$crawler->filter('h2')->eq(2);
166
Fetch Siblings
1
$crawler->filter('.question')->siblings();
Fetch Children
1
$crawler->filter('ul.tasks')->children();
How might you do that? Well, one way would be to use a CSS selector to hunt down the applicable unordered list, map over the list of nodes, and extract the text content for each.
1 2 3 4 5 6 7 8 9 10 11 12
// Call route $crawler = $this->client->request('GET', '/tasks'); // Filter down to the desired list $items = $crawler->filter('ul.tasks li'); // Map over of the list, and return // the text content for each. $tasks = $items->each(function($node, $i) { return $node->text(); });
167
.array(3) { [0]=> string(11) "Go to store" [1]=> string(11) "Finish book" [2]=> string(10) "Eat dinner" }
But, we can do better. The extract method can be used to pull out everything from attribute values, to the text content itself. As such, rather than mapping over the list items with each(), we can instead clean things up a bit, like so:
1 2
$items = $crawler->filter('ul.tasks li'); $tasks = $items->extract('_text'); _text is a special attribute name that refers to the nodes value. This will achieve the exact same
result. If you need to grab a different attribute value from a node, simply substitute _text with its name, accordingly. Heres a few examples:
1 2 3
Forms
Another helpful functionality that the DomCrawler component provides is the ability to fill out and submit forms.
1 2 3 4 5 6 7 8 9 10
public function testRegister() { $crawler = $this->client->request('GET', '/register'); // Find the form associated with the submit button // that has a value of 'Submit' $form = $crawler->selectButton('Submit')->form(); // Fill out the form $form['first'] = 'Jeffrey';
168
This component can absolutely be helpful in certain situations; however, from my experiences, I prefer to use a more streamlined and readable tool for querying the DOM: Codeception. Well discuss this tool extensively in one of the following chapters. Until then, have fun with this!
Summary
There are those who would argue that testing controllers in this way is unnecessary. Let your functional and acceptance tests, theyd say, verify the routes and view bindings. As always, these things boil down to preference and testing style. Personally, I like to keep them. One alternate approach, however, is to remove the mocks and instead treat these as true functional tests, similar to what the Ruby on Rails community advocates. Setting up a test database (in memory, if you can) for your controller tests, though slower, has the benefit of making the tests significantly easier to write and prepare. Its up to you! Make up your own mind.
IoC: An IoC container provides an easy interface for managing the creation of complex objects, without sacrificing on code readability or terseness. At its core, it allows you to abstract complicated instantiation behind a single line. Dependency injection is an implementation of the IoC pattern. Even better, Laravels implementation is one of the most powerful in the PHP community. Outside of Laravel, Pimple is quite popular, as well.
Dependency Injection?
Laravels IoC container makes the process of leveraging dependency injection to build testable applications as easy as possible. Dependency injection is a pattern for inserting a class dependencies through its constructor (or a setter method), rather than hardcoding them - which, as you might have guessed, makes testability a considerable issue. While you can certainly leverage dependency injection without the IoC container, it sure does make the task a lot easier!
Definition: Think of dependency injection as a way to allow your future self to inject mocks in place of those dependencies, for testing purposes.
Constructor Injection
http://pimple.sensiolabs.org/
169
170
Setter Injection
1 2 3 4
class MyCommand { public function fire() { $generator = new ModelGenerator; return $generator->make() ? 'foo' : 'bar'; } }
This fire method is difficult to test - if not impossible (not without installing Runkit). It can be improved by injecting an instance of ModelGenerator through the class constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13
class MyCommand { protected $generator; public function __construct(ModelGenerator $generator) { $this->generator = $generator; } public function fire() { return $this->generator->make() ? 'foo' : 'bar'; } }
https://github.com/zenovich/runkit/
171
Much better! Think of this as the command asking for the ModelGenerator. The test can now be tested quite easily: mock ModelGenerator, inject it into the class, and perform an expectation that the make method is called, along with your desired return value.
1 2 3 4 5 6 7 8 9
public function testFire() { $gen = Mockery::mock('ModelGenerator'); $gen->shouldReceive('make')->once()->andReturn(true); $command = new MyCommand($gen); $this->assertEquals('foo', $command->fire()); }
See? Not too hard. Dependency injection may initially be a scary term, but its actually quite descriptive: inject dependencies into a class.
Resolving
Lets review a second example. Consider the following controller.
1 2 3 4 5 6 7 8 9 10
<?php class UsersController extends BaseController { public function index() { $users = User::all(); return View::make('users.index', ['users' => $users]); } }
Theres nothing overly wrong with this code. In fact, youve surely written it, in some form or another, many, many times! However, as soon as testability becomes a first-class citizen, this code will no longer suffice. Why? Well, how would you test it without physically hitting the database? Theres a couple hacks that you might use, but they involve alias mocks and multiple PHP processes. Like crossing the streams in Ghostbusters, its just not a good idea. There are a couple ways to manage this.
172
Solution 1: Defaults
Lets assume that, for a particular project, no IoC container is available. In situations such as this, allowing for testable code also has the adverse effect of forcing a laborious, less readable instantiation. Which would you prefer?
1
Or:
1 2 3 4
$dep1 = new Dependency; $dep2 = new OtherDependency; $thing = new Thing($dep1, $dep2);
One solution is to set a sensible default, while still allowing for constructor injection. Heres an example:
1 2 3 4 5 6
function __construct(Dependency $dep1 = null, OtherDependency $dep2 = null) { $this->dep1 = $dep1 ?: new Dependency; $this->dep2 = $dep2 ?: new OtherDependency; }
With this technique, we have the flexibility to inject mock objects for both Dependency and OtherDependency. On the flip-side, if we stick with new Thing;, those objects will still be instantiated. For small projects that dont have the luxury of a dedicated container (even though theyre freely available on Packagist), this may be a preferable choice.
Solution 2: Resolving
This is a Laravel book, so lets solve it the Laravel way! A first step to improving this code might be to first resolve the User model out of the IoC container, rather than referencing the class directly. The following code resolves an instance of User, and then triggers its all method. Because one isnt currently registered, Laravel will simply fetch the User object and return the object.
173
public function index() { $users = App::make('User')->all(); return View::make('users.index', ['users' => $users]); }
Well discuss binding/resolving more shortly. The most important piece to understand at the moment is that were asking Laravel to resolve an instance of User out of the IoC container. When testing the method, Eloquent can be avoided all together (why would we have any interest in testing that? ) by swapping out the currently registered instance of User with a mock, like so:
1 2 3 4 5
With this modification, the next time that App::make('User') is called, Laravel will return this new mock object. In this case, as the tested code will require access to an all method, we provide one that simply returns foo. Dont forget that if an array is passed as the first argument to an anonymous mock, then Mockery will register the keys of that array as methods, and their respective values as the methods returned values. Heres an example:
1 2
Should you need to perform expectations on the mock (you likely will), then the test may be modified, like so:
1 2 3 4 5 6 7 8
public function testIndex() { $mock = Mockery::mock(); $mock->shouldReceive('all')->once()->andReturn('foo'); App::instance('User', $mock); $this->call('GET', 'users'); }
Repeating this logic for each test can get tedious, though. We can DRY things up by extracting it to its own method that will run before each test, like so:
174
<?php class UsersControllerTest extends TestCase { public function tearDown() { Mockery::close(); } protected function mock($class) { // Create a mock object, and register the // instance with the IoC container $this->mock = Mockery::mock(); App::instance($class, $this->mock); } public function testIndex() { // Set an expectation that the all() method // should be called at least once. $this->mock->shouldReceive('all')->once(); $this->call('GET', 'users'); } }
Now, each test only needs to declare its expectations on the model, and then call the route, per usual.
App Bindings
There will likely be times when you need more control over how classes are resolved out of the container (or so you might think). You can register a binding with the app, and pass a closure as the second argument, which will dictate how this binding will be resolved. Heres a contrived example that binds the key, foo, to a closure that will instantiate the IKnowKungFoo class.
175
<?php // app/routes.php class IKnowKungFoo {} // Only for example. Don't do this. App::bind('foo', function() { return new IKnowKungFoo; });
To trigger that closure and return the new object, use App::make('foo').
1 2 3 4
Laravel offers an alternate syntax for binding keys to classes. Rather than passing a closure, we could instead reference the name of the class to instantiate, as a string. The following will achieve the exact same result.
1
App::bind('foo', 'IKnowKungFoo');
Now, in a real-world situation, theres no need for this. Because were not performing any real logic within the closure, we could simply do:
1 2 3 4
When resolving IKnowKungFoo, if Laravel doesnt find an existing registered binding, itll next look for a class name, called IKnowKungFoo. If it finds one, it will simply return a new instance. On the other hand, if no class exists either, then Laravel has no idea what to do, in which case a ReflectionException will be thrown:
176
Interfaces
You might be thinking to yourself, When would this ever be useful? Well, as it turns out, the answer is quite frequently - particular when coding to an interface. Imagine a controller that asks for an implementation of OrderRepositoryInterface. Repositories represent the data-access layer of your application.
1 2 3 4 5 6
Because controllers, too, are resolved out of the IoC container, Laravel, when presented with this code, will come to a road block, and throw a BindingResolutionException. Why? Because its attempting to instantiate an interface, which obviously isnt allowed. In situations such as this, the framework needs a bit more information from us. Think of this exception as the frameworks way of saying:
177
Okay, your controller is requesting an implementation of OrderRepositoryInterface but you havent told me which one to use. Im adding this to Fail Blog, son. Perhaps you want to use an Eloquent-specific implementation of the interface: EloquentOrderRepository. To use it, create a new binding to make a connection between the interface and its implementation.
1
App::bind('OrderRepositoryInterface', 'EloquentOrderRepository');
To reiterate, think of this as your way of saying: Yo, Laravel - when you need an instance of OrderRepositoryInterface, I want you to use EloquentOrderRepository. K bro? Remember, this line of code is identical to:
1 2 3 4
Now, when the OrdersController is instantiated, Laravel will correctly pull in the Eloquent implementation of the repository. Why is this so useful? Because, when testing controllers (which youll soon learn how to do), by allowing for this level of flexibility, injecting a mocked version of the repository will be a cinch!
178
App::bind('UsersController', function() { $repository = new Repositories\User; $validator = new Services\Validators\User; return new UsersController($repository, $validator); });
For experimentation purposes, you may continue to throw this code in your routes file; however, for real-world projects, youll likely want to create a service provider to register these types of bindings. Nonetheless, if we run
1
$usersController = App::make('UsersController');
the closure will fire, and return a new instance of UsersController with those two dependencies. Fine - we already know this. But, wait: Laravel is a smart little cookie! Through a technique referred to as automatic resolution, Laravel can do the brunt of the work for you, thanks to the power of PHP reflection. Okay, too much jargon here; lets simplify things. When resolving an object through the IoC container, Laravel will attempt to read the constructors type-hints and automatically inject the instance for you.
Definition: PHP 5 comes with a complete reflection API that adds the ability to reverse-engineer classes, interfaces, functions, methods and extensions. Additionally, the reflection API offers ways to retrieve doc comments for functions, classes and methods. - php.net
When resolved, Laravel will detect that instances of both UserRepository and Validator are required. It will then instantiate and inject them automagically! Folks, this is a very cool thing. I repeat: Laravel will automatically inject instances of those two classes for you; no manual binding necessary.
http://www.php.net/manual/en/intro.reflection.php
179
Extra Credit
If youd like to learn more about how Laravel allows for this, have a look at Illuminate/Container/Container - specifically, the build method. See below (but dont worry if its overwhelming).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
<?php // vendor/laravel/framework/src/Illuminate/Container/Container.php /** * Instantiate a concrete instance of the given type. * * @param string $concrete * @param array $parameters * @return mixed */ public function build($concrete, $parameters = array()) { // If the concrete type is actually a Closure, we will just // execute it and hand back the results of the functions, // which allows functions to be used as resolvers for more // fine-tuned resolution of these objects. if ($concrete instanceof Closure) { return $concrete($this, $parameters); } $reflector = new \ReflectionClass($concrete); // // // // if { If the type is not instantiable, the developer is attempting to resolve an abstract type such as an Interface of Abstract Class and there is no binding registered for the abstractions so we need to bail out. ( ! $reflector->isInstantiable()) $message = "Target [$concrete] is not instantiable."; throw new BindingResolutionException($message); } $constructor = $reflector->getConstructor(); // If there are no constructors, that means there are no // dependencies then we can just resolve the instances of the
180
// objects right away, without resolving any other types or // dependencies out of these containers. if (is_null($constructor)) { return new $concrete; } $parameters = $constructor->getParameters(); // Once we have all the constructor's parameters we can create // each of the dependency instances and then use the reflection // instances to make a new instance of this class, injecting the // created dependencies in. $dependencies = $this->getDependencies($parameters); return $reflector->newInstanceArgs($dependencies); }
// Unnecessary App::bind('UsersController', function() { $repository = new Repositories\User; $validator = new Services\Validators\User; return new UsersController($repository, $validator); });
Laravel will automatically do this for you. Its only necessary in situations when the instantiation process is a bit too specific for Laravel to figure out automatically.
Summary
When researching inversion of control on the internet, youll likely come across dozens of incredibly confusing articles and definitions. Make it easy on yourself: Laravels IoC container is simply a tool that reverses the direction of responsibility, by assisting with class dependency management. When embraced, your applications will become significantly more flexible and testable. Use it.
Commands 101
Before we dig into testing commands, lets first review the basic process of creating them.
Scaffolding
Artisan ships with a nifty generator that can scaffold the necessary boilerplate code for new commands. For instance, to create the skeleton for a command that expedites the process of adding new users to a users table, from the command-line, run:
1
<?php use Illuminate\Console\Command; use Symfony\Component\Console\InputInputOption; use Symfony\Component\Console\InputInputArgument; class UserCreatorCommand extends Command { protected $name = 'command:name'; protected $description = 'Command description.'; public function __construct() {
http://symfony.com/doc/2.0/components/console/introduction.html
181
182
parent::__construct(); } public function fire() {} protected function getArguments() { return array( array( 'example', InputArgument::REQUIRED, 'An example argument.' ) ); } protected function getOptions() { return array( array( 'example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null ) ); } }
The values provided here will be referenced when running php artisan from the command line.
Tip: The console component also supports pseudo-namespacing. To place multiple commands under a migrate heading, as Laravel does by default, youd name them as migrate:install, migrate:make, etc. When running php artisan, they will be grouped.
183
protected $name = 'user:create'; protected $description = 'Create a new record in the users table.';
However, adjusting these values alone wont make the command show up when running php artisan; it first has to be registered with Artisan, which can be done from app/start/artisan.php.
1 2 3
That should do it! If we now list the available Artisan commands with php artisan, the user:create command should now display at the bottom. Its so easy!
Arguments
The console component makes the process of passing arguments and options to your command a cinch. Specify any available arguments within the getArguments method, like so:
184
/** * Get the console command arguments. * * @return array */ protected function getArguments() { return array( array( 'name', InputArgument::REQUIRED, 'Name of the user' ) ); }
Because this code specifies that the name argument is required, via InputArgument::REQUIRED (the inverse being InputArgument::OPTIONAL), if we attempt to run the command without arguments, a RuntimeException will be thrown.
185
Should you require additional arguments, simply return a comma-separated list of arrays from the getArguments method. To capture the value of the supplied name from your code, use $this->argument(KEY). The fire method on the class will automatically be triggered when the command is called from the commandline. Heres an example of capturing the name argument, and printing the response to the console.
1 2 3 4 5 6 7 8 9 10 11 12 13
// app/commands/UserCreatorCommand.php /** * Execute the console command. * * @return void */ public function fire() { $name = $this->argument('name'); $this->info("The name that you want to add is {$name}."); }
186
Tip: The Command class provides a handful of helper methods to print strings to the console, including, but not limited to, info(), error(), and line(). Refer to the Illuminate\Console\Console class for a full list.
The users of your command can view a list of the arguments and options, via the help dialog.
1
Options
Options are just that: optional. They can be helpful for specifying flags which alter how a particular action is executed. Perhaps your model generator (which well build using TDD in the next section) should accept an optional path to the models directory. You can accept this value, via the getOptions method, like so:
187
/** * Get the console command options. * * @return array */ protected function getOptions() { return array( array( 'path', 'p', InputOption::VALUE_OPTIONAL, 'Path to models directory', 'app/models' ) ); }
Each option array can accept five parameters: Name of the option Optional alias: --path becomes -p Option variants, including VALUE_OPTIONAL, VALUE_REQUIRED, VALUE_NONE, VALUE_IS_ARRAY Description for help dialog Optional default value
Updating this single method now allows the user to override the default path to the models directory. Heres an example, assuming a generate:model command:
1
Alternatively, the user may instead reference the alias that we specified:
1
If the path option is specified, the value provided should be used as the path to the models directory; otherwise, our default of app/models will be used.
188
Exercise
Now that the basics of creating commands are instilled, lets move on to the chapter exercise. Well use test-driven development to build a file generator that is similar to my popular Laravel Generators tool. More specifically, well create a package that allows the user to rapidly generate a new model, using a base template as its contents. Chances are high that, when writing new commands, youll want to share them. The easiest way to do so is to create a package, using Laravels helpful workbench, push it to GitHub, and make it available on Packagist. This means that well need to follow a slightly different process than was covered earlier in this chapter.
https://github.com/JeffreyWay/Laravel-4-Generators
189
<?php // app/config/workbench.php return array( 'name' => 'Jeffrey Way', 'email' => '[email protected]', );
Next, running php artisan help workbench, we can see that the command expects one argument: the name of the vendor and package.
1 2
$ php artisan help workbench package The name (vendor/name) of the package.
Typically, vendor will be the name of your company. Alternatively, feel free to use any identifier, or even your last name, as I do. Lets try it:
1 2 3 4 5 6 7 8 9
$ php artisan workbench way/generators Package workbench created! Loading composer repositories with package information Installing dependencies (including require-dev) - Installing illuminate/support (dev-master cc16ed1) Cloning cc16ed1445c39846c1c9aaccfc9f1648b28bf3c3 Writing lock file Generating autoload files
This will generate a new workbench directory, along with the necessary folder structure (following PSR-0) and files for rapidly preparing your package.
190
191
Service Providers
In the previous section, we used app/start/artisan.php to register the command with Artisan. This time, though, well store the instantiation process within a service provider. When Laravel scaffolded your new package, it included a service provider class, GeneratorsServiceProvider. Think of this file as the bootstrap for your package.
Definition: Service providers are simply bootstrap classes for packages. By default, they contain two methods: boot and register. Within these methods you may do anything you like: include a routes file, register bindings in the IoC container, attach to events, or anything else you wish to do.
In the code below, we register the packages Artisan commands, via the commands method on the ServiceProvider class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?php // workbench/.../Way/Generators/GeneratorsServiceProvider.php namespace Way\Generators; use Illuminate\Support\ServiceProvider; class GeneratorsServiceProvider extends ServiceProvider { protected $defer = false; public function register() { $this->registerModelGeneratorCommand(); $this->commands( 'generate.model' ); } protected function registerModelGeneratorCommand() {
192
The final step - one that every user of the package must perform manually - is to add the service provider to the providers array in app/config/app.php.
1 2 3 4 5 6
All service providers listed here will automatically be loaded by Laravel. It should now work! Run php artisan to ensure that it does.
193
Tip: If distributing this package through Packagist, youll want to make a note in your readme file that the user needs to add the necessary service provider to app/config/app.php.
"require": { "php": ">=5.3.0", "illuminate/support": "4.0.x", "illuminate/console": "4.0.x" }, "require-dev": { "mockery/mockery": "dev-master" }
This specifies that the package expects to have access to Illuminates console component, as well as Mockery to behave as expected. Run composer update to pull in both of these. Next, well write the first test. When the workbench command generated the package, it also included a folder for any tests. Within that directory, add a new commands/ModelGeneratorCommandTest.php file.
194
Notice how the directory structure of our tests mirror the production files.
Testing output to a console can get a bit tricky with PHPUnit, but, luckily, the Symfony team have already considered this. Their console component provides a test helper that eases the process considerably. For example, to test that a dummy command simply outputs The name argument is foo, we could write:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php // workbench/.../tests/commands/ModelGeneratorCommandTest.php use Way\Generators\Commands\ModelGeneratorCommand; use Symfony\Component\Console\Tester\CommandTester; class ModelGeneratorCommandTest extends PHPUnit_Framework_TestCase { public function testOutput() { $tester = new CommandTester(new ModelGeneratorCommand); # Pass in any arguments or options $tester->execute(['name' => 'foo']); $this->assertEquals( "The name argument is foo\n",
195
$tester->getDisplay() ); } }
The most important bit of this code is that we instantiate Symfonys CommandTester class, and pass in an instance of the command that we wish to test - in this case, ModelGeneratorCommand. We then trigger its execute method, providing any applicable arguments. Eventually, behind the scenes, the fire method on your command will be triggered. Lastly, the getDisplay method on the CommandTester instance is used to fetch any output. In its current state, running phpunit will, of course, fail.
1 2 3 4 5 6 7
1) ModelGeneratorCommandTest::testOutput Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'The name argument is foo +''
To make it pass, from the ModelGeneratorCommand command, we only need to print the necessary string.
1 2 3 4 5 6
And that returns us to green! Congratulations; youve just tested your first dummy command.
Planning
Before writing a test, its always a smart idea to determine what you expect to happen. Thats one of the core benefits to a TDD cycle. It forces you to think before you write. In this case, when the command fires, I expect it to fetch the path and model name to generate, and then pass that data on to a special model generator class that will take care of the rest. This way, we limit the command classs responsibility. Perhaps the generator will return a boolean, indicating whether or not the model was created successfully.
Expectations
Now that we have a game plan, lets use Mockery to prepare the first test: testGeneratesModelSuccesfully.
196
<?php // workbench/.../tests/commands/ModelGeneratorCommandTest.php use Way\Generators\Commands\ModelGeneratorCommand; use Symfony\Component\Console\Tester\CommandTester; use Mockery as m; class ModelGeneratorCommandTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } public function testGeneratesModelSuccessfully() { # We're not interested in testing the model generator yet. $gen = m::mock('Way\Generators\Generators\ModelGenerator'); # We only want to ensure that its make() method is called. # We'll have it return true to mimic a positive outcome. $gen->shouldReceive('make') ->once() ->with('app/models/Foo.php') ->andReturn(true); $command = new ModelGeneratorCommand($gen); $tester = new CommandTester($command); $tester->execute(['name' => 'foo']); # Ensure that the proper response is printed. $this->assertEquals( "Created app/models/Foo.php\n", $tester->getDisplay() ); } }
Dependency Injection
The first step to making this test pass is to ensure that the generator can be injected into the ModelGeneratorCommand class through its constructor. This way, we can easily swap out the generator with the mocked version, as illustrated above.
197
// workbench/.../Generators/Commands/ModelGeneratorCommand.php <?php namespace Way\Generators\Commands; use use use use Way\Generators\Generators\ModelGenerator; Illuminate\Console\Command; Symfony\Component\Console\Input\InputOption; Symfony\Component\Console\Input\InputArgument;
class ModelGeneratorCommand extends Command { protected $name = 'generate:model'; protected $description = 'Generate a new model.'; protected $generator; public function __construct(ModelGenerator $generator) { parent::__construct(); $this->generator = $generator; } // ... }
Generator Class
The next step is to create the generator class that will handle the process of creating the model with the necessary boilerplate code. This will be placed in the Way/Generators/Generators directory.
1 2 3 4 5 6 7
That will do for now. Remember: were testing the command class, not the generator itself. That will have its own tests, so dont double-up!
198
// workbench/.../Generators/Commands/ModelGeneratorCommand.php <?php namespace Way\Generators\Commands; use use use use Way\Generators\Generators\ModelGenerator; Illuminate\Console\Command; Symfony\Component\Console\Input\InputOption; Symfony\Component\Console\Input\InputArgument;
class ModelGeneratorCommand extends Command { protected $name = 'generate:model'; protected $description = 'Generate a new model.'; protected $generator; public function __construct(ModelGenerator $generator) { parent::__construct(); $this->generator = $generator; } public function fire() { $path = $this->getPath(); if ($this->generator->make($path)) { $this->info("Created {$path}"); } } protected function getPath() { return $this->option('path') . '/' . ucwords($this->argument('name'\
199
)) . '.php'; } protected function getArguments() { return array( array( 'name', InputArgument::REQUIRED, 'Name of the model to generate.' ), ); } protected function getOptions() { return array( array( 'path', null, InputOption::VALUE_OPTIONAL, 'Path to the models directory.', 'app/models' ) ); } }
Even though we havent yet built the generator class beyond its skeleton, phpunit should return green! The reason is because the generator is being mocked with Mockery. The test merely ensures that the make method on the generator class is called, and the necessary response is printed to the console, assuming that the file was generated successfully. But, what if the model generation was not successful? We need a test for that, too!
Tip: Always provide a test for each path through your code.
200
// workbench/.../tests/commands/ModelGeneratorCommandTest.php public function testAlertsUserIfModelGenerationFails() { $gen = m::mock('Way\Generators\Generators\ModelGenerator'); # This time, simulate a failed result $gen->shouldReceive('make') ->once() ->with('app/models/Foo.php') ->andReturn(false); $command = new ModelGeneratorCommand($gen); $tester = new CommandTester($command); $tester->execute(['name' => 'foo']); # If generation failed, the output should indicate as much. $this->assertEquals( "Could not create app/models/Foo.php\n", $tester->getDisplay() ); }
// workbench/.../Generators/Commands/ModelGeneratorCommand.php public function fire() { $path = $this->getPath(); if ($this->generator->make($path)) { return $this->info("Created {$path}"); } $this->error("Could not create {$path}"); }
The final test for this class will assert that, if a custom path option is specified by the user, the getPath method will respond, accordingly. Im fairly certain that, as the code stands, this will already work, but always feel free to write additional tests - if only for peace of mind.
201
// workbench/.../tests/commands/ModelGeneratorCommandTest.php public function testCanAcceptCustomPathToModelsDirectory() { $gen = m::mock('Way\Generators\Generators\ModelGenerator'); # Ensure that the custom path to the directory is correct $gen->shouldReceive('make') ->once() ->with('app/foo/models/Foo.php'); $command = new ModelGeneratorCommand($gen); $tester = new CommandTester($command); $tester->execute(['name' => 'foo', '--path' => 'app/foo/models']); }
Without a single change, phpunit still returns green! The only remaining thing to do is ensure that, when the ModelGeneratorCommand class is registered with Artisan in the service provider, we also inject the ModelGenerator class. Remember: we got away without doing this, because we manually injected a mocked version of the model generator class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<?php // workbench/.../Way/Generators/GeneratorsServiceProvider.php namespace Way\Generators; use Way\Generators\Commands\ModelGeneratorCommand; use Way\Generators\Generators\ModelGenerator; use Illuminate\Support\ServiceProvider; class GeneratorsServiceProvider extends ServiceProvider { protected $defer = false; public function register() { $this->registerModelGeneratorCommand(); $this->commands( 'generate.model' ); }
202
protected function registerModelGeneratorCommand() { $this->app['generate.model'] = $this->app->share(function($app) { # Inject the generator into the command return new ModelGeneratorCommand(new ModelGenerator); }); } }
And with that, the ModelGeneratorCommand class (along with its associated tests) is finished! But, certainly, were not done. So far, weve only mocked the model generator for the purposes of testing the command. Next, we need to test the generator, itself.
<?php // tests/generators/ModelGeneratorTest.php use Way\Generators\Generators\ModelGenerator; use Mockery as m; class ModelGeneratorTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } }
The primary purpose of this class is to facilitate the process of compiling a model template with the values provided by the user, and physically creating the file within the proper models directory. With that in mind, the first test will be called testCanGenerateModelUsingTemplate.
1 2 3
We must be careful that our tests dont actually create new files. This serves no purpose other than to slow down the tests. This means that well need to mock Laravels Filesystem class; however, in
203
order to make use of that class in our package, it needs to be required as a dependency. Dont forget that Laravel is composed of multiple components. To pull in this package, simply require it within the packages composer.json file, and run composer
update.
1 2 3 4 5 6
Next, we need some means to verify that the compiled template matches our expected output. To allow for this, well add a tests/generators/stubs directory. This folder will only house a model.txt stub, for the purposes of this tutorial.
1 2 3 4 5
The contents of this file represent the output that we expect to be written, when php artisan generate:model foo is executed. Now, we can simply mock the Filesystem, and ensure that its put method is called with the correct path and data, accordingly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// tests/generators/ModelGeneratorTest.php public function testCanGenerateModelUsingTemplate() { $file = m::mock('Illuminate\Filesystem\Filesystem[put]'); $file->shouldReceive('put') ->once() ->with('app/models/Foo.php', file_get_contents(__DIR__.'/stubs/mod\ el.txt')); $generator = new ModelGenerator($file); $generator->make('app/models/Foo.php'); }
204
Tip: Mockery allows for partial mocks, by specifying a comma separated list of methods to mock within brackets. All unlisted methods will continue to behave as they normally would.
The code above specifies that, when the make method on the model generator is called, we expect that the put method on Laravels helpful Filesystem class will be called. More importantly, though, it should be called with the correct arguments: the name of the model to generate, and the contents of the model stub that we created earlier. Running the tests, as you might expect, will return red.
1 2 3 4 5 6 7 8 9 10
$ phpunit 1) ModelGeneratorTest::testCanGenerateModelUsingTemplate Mockery\Exception\InvalidCountException: Method put("app/models/Foo.php", "\ <?php class Foo extends Eloquent { }") from Illuminate\Filesystem\Filesystem should be called exactly 1 times but called 0 times.
It seems that Mockery expected the put method to be called with the proper arguments, but that never occurred. In order to make the test pass, we need a new templates directory that will house a stub for each generator (we only have one for this chapter, but you could certainly add more on your own). Heres the template for generating a new model.
1 2 3 4 5
Notice the {{name}} portion? Think of it as a placeholder that will ultimately be replaced with the model name designated by the user, when running the command. Assuming php artisan generate:model foo, {{name}} will be replaced with Foo. Now, we have a link between the model template and the test stub. If it was compiled correctly, the two should match! At this point, we only need to write the necessary logic to fetch the compiled template, and pass its contents on to the Filesystems put method. Give the following snippet some study time.
205
<?php // workbench/.../Generators/Generators/ModelGenerator.php namespace Way\Generators\Generators; use Illuminate\Filesystem\Filesystem as File; class ModelGenerator { protected $file; public function __construct(File $file) { $this->file = $file; } public function make($path) { $name = basename($path, '.php'); $template = $this->getTemplate($name); if (! $this->file->exists($path)) { return $this->file->put($path, $template); } return false; } protected function getTemplate($name) { $template = $this->file->get(__DIR__.'/templates/model.txt'); return str_replace('{{name}}', $name, $template); } }
The code is really quite simple. The getTemplate method fetches the model template and runs a quick a search and replace on it, using the users provided model name. The results of this operation will be passed on to the Filesystems put method, which weve mocked for the test. As noted earlier, we dont want to physically create this file; we merely need to ensure that the appropriate method was, in fact, called. Thats all it takes! Were back to green! Dont forget, though, that, because the model generator expects an instance of Filesystem upon instantiation, we need to update the service provider to
206
pass it in.
1 2 3 4 5 6 7 8 9 10 11
// workbench/.../Way/Generators/GeneratorsServiceProvider.php protected function registerModelGenerator() { $this->app['generate.model'] = $this->app->share(function($app) { $generator = new ModelGenerator($app['files']); return new ModelGeneratorCommand($generator); }); }
That should do it! The tests pass; the only remaining thing to do is try it out for real! cd to the root of your application, and run:
1
As our tests proved would happen, app/models/Foo.php will be created with the following boilerplate:
1 2 3 4 5
Go ahead and try out a few inversions of the command to prove that everything is functioning as expected.
207
Great work!
Summary
In this chapter, we leveraged Symfonys Console component to build a model generator with tests. Testing console output can be a bit tricky, but, luckily, the Symfony team already considered this, and included a CommandTester class to ease the process considerably. Now, even your custom Artisan commands will be test-driven!
APIs in Laravel
One of the wonderful things about Laravel is that it was so clearly created (and updated) to remedy real-world problems and irritations. As a result, often, youll find that implementing a particular piece of functionality doesnt require more than a few moments. Building APIs is no different. In fact, as I was writing the demo API for this chapter, I found myself laughing at how easy it is. Even five years ago, in PHP, this would have required the better part of a day to implement. Luckily, those days are gone.
1. Authentication
One of Laravel 4s excellent new features is support for basic HTTP authentication. In addition to offering an easy way to create a private section of your web site (when a full login system isnt necessary), HTTP-based authentication can also be a perfect choice for simple APIs. Triggering basic auth only requires that you reference a native filter: auth.basic. Heres an example:
1 2 3 4 5 6
// app/routes.php Route::get('admin', ['before' => 'auth.basic', function() { return 'Secret admin page.'; }]);
http://tutsplus.com
208
209
Believe it or not, thats all you need to password-protect a page or group! Laravel for the win!
HTTP authentication
To instead reference this filter from your controllers, use the beforeFilter method within the controllers constructor, like so:
1 2 3 4 5 6
Now, each route will be protected with basic authentication. So, how might we leverage this functionality for authenticating API users? Quite simply, in fact: create a resource, and reference the filter before all applicable routes. The route:
1 2 3
210
Alternatively, to prevent Laravel from setting a user identifier cookie in the session, you could create another filter within app/filters.php and reference that.
1 2 3 4
Tip: Basic authentication for an API is the easiest way of verifying a user, however, its not overly secure. For more significant APIs, youll likely want to instead opt for an API key and secret. Nonetheless, as this chapter isnt dedicated to API design, lets keep things simple and stick with basic HTTP authentication.
2. Route Prefixing
As this is an API, its a smart idea to future-proof it by nesting the associated routes, using a prefix, such as api/v1.
1 2 3
Pay close attention to the naming convention for this prefix. By adopting a consistent pattern, such as api/v1 or api/v2, were able to extend the APIs functionality in the future, while continuing to provide a consistent interface for those who are still dependent upon an older version of the API.
211
3. Return JSON
Though you could provide support for returning both XML and JSON from your API, why bother? Its 2013: stick with JSON-only. Besides, what kind of developer prefers XML over JSON when querying a web service? I havent met these fictional creatures. By default, when a collection is returned from a controller method, that data will be translated into JSON. This means, if you want to return a JSON-representation for a user with an id of 1, youd only need to write:
1 2 3 4
However, convenient as this is, when building APIs, youll likely want to provide additional feedback to the API-caller. To manually return a JSON response from a route, use the json method on the Response class. Heres an example that fetches all photos for the authenticated user, and returns a JSON response.
212
class PhotosApiController extends \BaseController { public function index() { return Response::json([ 'error' => false, 'photos' => Auth::user()->photos->toArray() ], 200); } }
Dont forget: once a user has been authenticated, you may access their associated user object with Auth::user(). In effect, to capture the id of the authenticated user, use Auth::user()->id. Assuming that all photos are publicly viewable, this will work perfectly; however, if your web app instead limits a photos visibility to the user associated with the photo, then dont forget to leverage the authentication filter that we reviewed in the previous section.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class PhotosApiController extends \BaseController { public function __construct() { $this->beforeFilter('auth.basic'); } public function index() { return Response::json([ 'error' => false, 'photos' => Auth::user()->photos->toArray() ], 200); } }
The easiest way to test a web service is with curl. In the image below, notice how, with very little code, we were able to correctly respond to incorrect and valid credentials.
http://curl.haxx.se/
213
Testing APIs
While, as youll find, testing an API is relatively straight-forward, there are, nonetheless, a few best practices to consider.
Did You Know?: When unit testing, Laravel will automatically set the environment to testing, and, subsequently, override all production configuration files with the ones stored in app/config/testing.
Assuming that your API is mostly CRUD-based, chances are high that you can opt for a Sqlite database in memory, which should provide a performance boost (though Ive seen mixed bench-
214
marks). To instruct Laravel to use a DB in memory, update app/config/testing/database.php to set a new default driver for testing.
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php // app/config/testing/database.php return array( 'default' => 'sqlite', 'connections' => array( 'sqlite' => array( 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ) ) );
Pay close attention to the fact that, rather than setting the database key to a .sqlite file, instead, a database in memory is specified. With this modification, when testing, Laravel will automatically set the environment to testing and reference the in-memory DB.
class PhotosApiTest extends TestCase { public function setUp() { parent::setUp(); Artisan::call('migrate'); $this->seed(); } }
The above code leverage the Artisan facade to call the migrate command (php artisan migrate). This will construct all necessary tables. Secondly. the seed() method is used to populate the tables with seed records (if applicable). Alternatively, you could do Artisan::call('db:seed').
215
Enable Filters
One pitfall that youll undoubtedly come across is when Laravel seemingly ignores route filters when unit testing. As a result, when testing your API, even though you may expect a test to fail authentication, PHPUnit will still return green (because the filter never fired)! Yikes! To compensate for this, before each test, always activate the filters.
1 2 3 4 5 6
Test Examples
With a few guidelines in place, lets review some examples!
216
// app/tests/api/PhotoApiTest.php class PhotoApiTest extends TestCase { public function setUp() { parent::setUp(); Route::enableFilters(); Artisan::call('migrate'); $this->seed(); Auth::loginUsingId(1); } public function testMustBeAuthenticated() { // Most tests will assume a logged in user // But not this one. Auth::logout(); $response = $this->call('GET', 'api/v1/photos'); $this->assertEquals('Invalid credentials.', $response->getContent()\ ); } }
217
public function testProvidesErrorFeedback() { $response = $this->call('GET', 'api/v1/photos'); $data = json_decode($response->getContent()); $this->assertEquals(false, $data->error); }
public function testFetchesAllPhotosForUser() { $response = $this->call('GET', 'api/v1/photos'); $content = $response->getContent(); $data = json_decode($content); // Did we receive valid JSON? $this->assertJson($content); // Decoded JSON should offer a photos array $this->assertInternalType('array', $data->photos); }
When testing an API, one of the most important assertions you can make is one that verifies whether real JSON was returned. Luckily, as of PHPUnit v3.7, an assertJson method is available.
Refactoring
Already, though, I can see that some cleanup is in order. If the tests return green, Im free to refactor. Lets break this into pieces. Heres a new test that verifies whether valid JSON was returned from the request.
218
public function testFetchesPhotos() { $response = $this->call('GET', 'api/v1/photos'); $data = json_decode($response->getContent()); $this->assertInternalType('array', $data->photos); }
Or, if youre feeling particularly rambunctious, you could write an assertion that decodes a JSON string, and validates whether a particular key exists on it. You crazy kid.
1 2 3 4 5 6 7 8 9 10 11 12 13
public function testFetchesPhotos() { $response = $this->call('GET', 'api/v1/photos'); $this->assertJsonStringHasKey('photos', $response->getContent()); } protected function assertJsonStringHasKey($key, $json) { $data = json_decode($json); $this->assertInternalType('array', $data->$key); }
Updating a Photo
Sure, fetching a list of your photos is great, but what about creating or updating? Making such a request with curl might take the form of:
219
When working with curl, -X refers to the request type (PUT, DELETE, etc.), and -d is for data that should be sent through. The following example will set a factory for a Photo and save it to the DB, and then call the necessary API route to update the record.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
public function testUpdatesExistingPhoto() { // Poor man's factory $photo = new Photo; $photo->caption = 'Some Photo'; $photo->path = 'foo.jpg'; $photo->user_id = 1; $photo->save(); $updatedPhotoFields = ['caption' => 'Updated Photo Caption']; $response = $this->call('PATCH', 'api/v1/photos/1', $updatedPhotoFields\ ); $data = json_decode($response->getContent()); $this->assertEquals('Photo has been updated', $data->message); $this->assertEquals('Updated Photo Caption', Photo::find(1)->caption); }
Notice how were passing the parameters as the third argument to the call method. From the controller, you can capture these values as you normally would when responding to a forms submission.
1 2
Once the route has been triggered, we merely inspect the returned JSON and ensure that the message is equal to Photo has been updated.
220
Factories
In the previous snippet, we used what Id refer to as a poor mans factory. Lets clean that up using my Factory class that we reviewed in a previous chapter. You can pull it into your project by either installing Laravel 4 Generators (which includes the package) or Laravel Test Helpers. Personally, I use the custom generators in every project, so thats what well use here.
Definition: In the simplest possible terms, a factory manages the creation of objects with dummy data.
// Poor man's factory $photo = new Photo; $photo->caption = 'Some Photo'; $photo->path = 'foo.jpg'; $photo->user_id = 1; $photo->save();
Way\Tests\Factory::create('Photo');
Or, better yet, use the class at the top of the file.
1 2 3
This will achieve the exact same result as what we had before, except, this time, the class will fake the values for each field. To override any fields, pass an array as the second argument to the create method, like so:
1
221
public function testUpdatesExistingPhoto() { Factory::create('Photo'); $updatedPhotoFields = ['caption' => 'Updated Photo Caption']; $response = $this->call('PUT', 'api/v1/photos/1', $updatedPhotoFields); $data = json_decode($response->getContent()); $this->assertEquals('Photo has been updated', $data->message); $this->assertEquals('Updated Photo Caption', Photo::find(1)->caption); }
Specifying Options
Basic API design dictates that options should be declared within the query string. Try not to use verbs within your URIs. As such:
1
api/v1/photos?color=green
api/v1/getGreenPhotos
Hopefully, that goes without saying. Lets write a test that specifies a limit for the number of photos that should be returned.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
public function testCanSetALimit() { // Give a new user two photos Factory::create('User'); Factory::create('Photo', ['user_id' => '1']); Factory::create('Photo', ['user_id' => '1']); // Declare a limit of 1 $response = $this->call('GET', 'api/v1/photos?limit=1'); $data = json_decode($response->getContent()); // Verify that the photos array count is 1, not 2 $this->assertCount(1, $data->photos); }
222
public function index() { $photos = Auth::user()->photos(); $photos = $this->applyOptions($photos); return Response::json([ 'error' => false, 'photos' => $photos->get()->toArray() ], 200); } protected function applyOptions($photos) { if ($limit = Request::get('limit')) $photos->take($limit); return $photos; }
Summary
One thing worth noting is that we exclusively relied upon functional (or some might refer to this as integration) testing. In a real-world project, youd likely want to create unit tests for the PhotosApiController class as well, providing the best of both worlds. In this chapter, we used a Sqlite database in memory and Factories to test a fictional photos API. As I noted at the beginning, the basic process should be quite familiar to you at this point. Really, its no different than any other type of testing: arrange, act, and then assert (AAA).
codeception.com
Codeception is kept as simple as possible for any kind of user. PHP developers, QAs, and managers can use Codeception. The only requirements are a basic knowledge of PHP, and theory of automated testing. Its configuration is kept short; all common issues are already solved. - codeception.com
http://codeception.com/
223
224
An Amuse Bouche
Before we dig further into how Codeception allows us to better test our applications, heres a quick teaser of the syntax. As a hello world example, lets say we want to ensure that, when /login is requested, the text, Login, is displayed. Using Codeception, we could write:
1 2 3
Notice how readable that is! Codeception places a significant emphasis on readability. Strip away as much clutter as possible, and get right down to what you need to test. It even goes so far as to recommend that you use an $I variable when instantiating your scenarios. This is because theyre meant to mimic the feature requests that your client might give you. For example:
1 2
While tools like Cucumber emphasize plain text scenarios, Codeception tweaks the formula just a bit to remove any potential duplication. Even better, if needed, it can auto-generate plain-text scenarios.
1
codecept generate:scenarios
But this is just the tip of the iceberg; Codeception can make assertions against whats stored in the database, perform AJAX requests, submit forms, click links, and much moreall using the same basic syntax demonstrated above.
Testing Refresher
So far in this book, weve mostly focused on testing in isolation. Theres a reason for this: roughly eighty percent of your tests should be unit tests. These need to be greased lightning fast, so that you can easily run the full suite each time a file is saved. Surprised that this percentage is so high? Well think about it: if we were building a time machine, wed definitely want to run some tests for ensuring that the car works correctly for the customer (Marty). For instance: Get in the car Put key into ignition and start car Turn on the time circuits
225
Set date to November 5th, 1955 Ensure that flux capacitor is fluxing Step on the gas Accelerate to 88mph Assert that electric sparks fly
This is an acceptance test! Notice how it flexes the entire system (or Delorean). This is probably a good test to write! But what happens if, for some reason, the car doesnt move when you step on the gas? In the real-world, slamming your head into the steering wheel wont magically fix things. How could you immediately detect what went wrong? It could be a loose wire, could be a faulty pedal, gas might not be getting to the engine thing (I know nothing about cars - hence, engine thing ), who knows? So it seems that testing from a bird-eye level, though incredibly important, also has the unavoidable side effect of being a high level test. From way up there, you cant see a thing. From this, we can deduce that, even though this test is essential, we also require lower level tests, which ensure that each component of the car - the starter, the fuel injection manifold (I know that one from Back to the Future 3), the radio - functions as expected. The key is to find the perfect ratio of unit tests, versus functional and acceptance tests. As a basic rule of thumb (remember that rules may be broken), plan on the number of unit test lines being the same as your production code. 80% unit, with the remainder dedicated to integration, functional, and acceptance tests.
Acceptance Testing
Acceptance tests execute code from the outside in, and will run in an environment that is as close to production as possible. This means that they will hit the database, query real web services, etc. In effect, they provide incredibly accurate and real-world results, but, as a side effect, have no choice but to be orders of magnitude slower than your unit tests, which can be executed in a matter of seconds. If these were the only tests you wrote, they would break the incremental TDD cycle. Waiting even ten seconds for tests to run is out of the question. To easily remember what acceptance testing refers to, lock on to the word, accept. When building new features, the customer (whoever that might be) will typically describe the functionality that they require. We refer to these as user stories.
1 2 3
As a user To more easily consume Tuts+ Premium content I want to download each course as a zip file
Notice how these stories are quite generalized - almost like a roadmap that charts out where you currently are, and where youre going. They dont delve into the specifics (when I view this page, I want to see a download button that links to the courses zip file). Theres a time and place to fetch
226
that information from the customer. But, to start, a user story should be condensed. Think of it as a newspapers two sentence description of some new movie thats out in theaters. Though it doesnt always play out this way in real life (not your fault), presumably, once this test passes, the feature has been fully implemented, and should be acceptable to the customer. If it isnt acceptable, then, at some point, wires became crossed, and the spec wasnt written correctly. Ive implemented the feature you requested. Do you accept that it works as expected? Tools like Cucumber were specifically written to give managers and quality assurance folks the ability to write these high-level tests for you, themselves. Its a cute end-goal, but, Ive yet to meet a team that functioned in this way. 95% of the time, youll find yourself (or someone else on your team) writing the acceptance test, based upon prior discussions with the clients.
Tools like Cucumber were specifically written to give managers and quality assurance folks the ability to write these high-level tests themselves.
Installation
As with any new tool, before we can dig in, we first need to install Codeception. Like Composer, there are a couple different ways to download it.
227
Global Installation
One way to install Codeception is to manually fetch its archive file.
1
wget http://codeception.com/codecept.phar
Next, youll probably want to move it somewhere permanent, so that it can be referenced globally.
1
mv codecept.phar /usr/local/bin/codecept
chmod +x /usr/local/bin/codecept
That should do it! Test it out by creating a new tab and running codecept. If installed correctly, youll see a list of all available commands.
228
Local Installation
A second way that we can pull in Codeception is through Composer. We first update the composer.json file to reference the Codeception package (Ill be using version 1.6.11 in this chapter).
1 2 3 4 5 6
composer update
And thats it! The codecept executable will be located at vendor/bin/codecept. To view the list of commands (as we did before), either add vendor/bin to your system path, or reference its full path, like so:
1
vendor/bin/codecept
Success!
https://packagist.org/packages/codeception/codeception
229
Now that weve successfully installed it on our system, lets do some testing.
Bootstrapping
Codeception is a full-stack testing framework and needs to be initialized for each new project.
1
codecept bootstrap
This command will dynamically generate a number of files within a tests/ directory. For languageagnostic projects, this will do just fine; however, for Laravel applications, we want these files to be placed within app/tests. As such, when bootstrapping, be sure to include the path argument to explicity set a custom install path.
1
Upon running this command, a suite of files will be added to the app/tests directory.
230
Specifically, make note of how tests are divided into unit, functional, and acceptance. As we reviewed earlier in this book, this is a popular convention that provides an easy way to execute a specific suite of tests. When writing unit tests, you likely dont want to wait for the slow acceptance tests to run as well!
class_name: WebGuy modules: enabled: - PhpBrowser - WebHelper config: PhpBrowser: url: 'http://localhost:8000'
While youre here, make a mental note that each suites functionality may be extended with modules. Well review this more shortly.
Generate a Test
Lets write one dummy test to observe the basic cycle, and then well move on to real-world tests. Tests may be created in two ways: manually or generated. The only difference is that, when using the latter approach, Codeception will provide a couple lines of boilerplate code to get you started. Either method will do just fine.
231
Manual Approach
Create a new file, app/tests/acceptance/WelcomeCept.php, and append:
1 2 3 4 5 6
<?php $I = new WebGuy($scenario); $I->wantTo('Check the home page for a welcome message'); $I->amOnPage('/'); $I->see('Welcome');
Generator Approach
From the command line, run:
1
If this command is run from the root of your Laravel application, a configuration exception will be thrown, noting that Codeception could not locate the codeception.yml file. This is because we set a custom path to the install directory. To compensate, use the -c option to set the path to the configuration file as well.
1
This can get cumbersome, though. Thats a lot to write for the sole purpose of generating a single file with two lines of boilerplate code. Lets make a couple of changes to remove the need to specify the path to the configuration file. Move app/codeception.yml to the root of your project.
1
mv app/codeception.yml codeception.yml
Next, update this file, and, within the paths object, replace all references to tests/ with app/tests, like so:
232
Both the manual and generate approach will achieve the same end result. Its up to you to decide which method you most prefer.
233
<?php $I = new WebGuy($scenario); $I->wantTo('Check the home page for a welcome message'); $I->amOnPage('/'); $I->see('Welcome');
The wonderful thing about scenario-based tests in Codeception is that the methods above dont require much explanation; theyre self explanatory. Notice how all tests are written in present tense - $I->see() rather than $I->shouldSee(). The only method that may require further explanation is wantTo(). Though it isnt required, it does help to provide a better description for the test, when viewing the results. For example: With:
1
Without:
1
1) WelcomeCept.php
As a best practice, opt for readability and always include this method call.
codecept run
234
Running tests
In the image above, notice how Codeception will highlight the scenario step that it failed on. This can be incredibly helpful when determining exactly what went wrong. In this case, Codeception failed to find Welcome in the response. Technically, this is a bit misleading. Even if no / route exists, step one will seemingly pass. To ensure that the response code is correct, you may use the seeResponseCodeIs(200) method. To make the test pass, we only need to create a route that returns the string, Welcome.
1 2 3 4 5 6
235
Suite acceptance started Trying to check the home page for a welcome message (WelcomeCept.php) - Ok Suite functional started Suite unit started Time: 0 seconds, Memory: 9.50Mb OK (1 test, 2 assertions)
Its so easy!
Tip: By default, all test suites will be triggered when running codecept run. To override this, as the first argument to the run command, specify the name of the suite to run: codecept run acceptance. This will, in effect, only trigger the tests within app/tests/acceptance. For further filtering, a second argument may be passed, which will specify the name of the single test to run. Dont forget that you can view the documentation for any command by preceding its name with help: codecept help run.
Summary
In this chapter, we reviewed the absolute basics of testing with Codeception, but, clearly, there is much more to cover. With the basic syntax under your fingers, in the next Exercise chapter, well continue using acceptance testing to build a relatively simple login form with authentication.
The Feature
Lets imagine that our fictional customer would like to have a new private (password-protected) administration area for their website. The user story might be:
1 2 3
In order to perform administrative tasks As the site owner I want to login to a password-protected private area
Using Codeception, the above user story may be translated to: 236
237
Think of the code above less as performing a specific action, and more defining the story background. At this point, at a high level, we can write code to interact with the web application in the same way that a human being might. This is why these types of tests are referred to as executing from the outside-in: they exercise the entire application, rather than one or two components (which would be integration testing). Below is our first test for logging in with proper credentials.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php // app/tests/acceptance/LoginCest.php class LoginCest { public function logsInUserWithProperCredentials(WebGuy $I) { $I->am('Site Owner'); $I->wantTo('login to a password-protected area'); $I->lookForwardTo('perform administrative tasks'); $I->amOnPage('/admin'); $I->seeCurrentUrlEquals('/login'); $I->fillField('email', '[email protected]'); $I->fillField('password', '1324'); $I->click('Login'); $I->seeCurrentUrlEquals('/admin'); $I->see('Admin Area', 'h1'); } }
Again, notice how, even if you werent a developer, you could probably figure out what this code does. 1. Vist the admin page, but expect to be redirected to the login page, as it should be protected. 2. Fill out the username and password, and click the Login button 3. Expect to be redirected to /admin, and see the text, Admin Area Running the tests at this point will, of course, lead to failure.
238
1) Couldn't login to a password-protected area in LoginCest.loginWithProper\ Credentials Guy couldn't see current url equals "/login": Failed asserting that two str\ ings are equal. --- Expected +++ Actual @@ @@ -'/login' +'/admin' Scenario Steps: 4. I see current url equals "/login" <-- RED 3. I am on page "/admin" 2. So that I perform administrative tasks 1. As a Site Owner
Register Routes
According to the tests, we require two routes: 1. /admin - Should be auth protected 2. /login - For creating a new user session Per usual, these can be added to app/routes.php.
1 2 3 4 5 6 7 8 9 10 11 12
<?php // app/routes.php Route::get('admin', ['before' => 'auth', function() { // Temporary return '<h1>Admin Area</h1>'; }]); Route::get('login', function() { return View::make('sessions.create'); });
239
1) Couldn't login to a password-protected area in LoginCest.loginWithProper\ Credentials Guy couldn't fill field "email","[email protected]": Field matching id|nam\ e|label|value or css or xpath selector does not exist Scenario Steps: 5. I fill field "email","[email protected]" <-- RED 4. I see current url equals "/login" 3. I am on page "/admin" 2. So that I perform administrative tasks 1. As a Site Owner
<!-- app/views/sessions/new.blade.php --> <!doctype html> <html> <head> <meta charset="utf-8"> <title>Login</title> </head> <body> <h1>Login</h1> {{ Form::open() }} <div> {{ Form::label('email', 'Email') }} {{ Form::text('email') }} </div> <div> {{ Form::label('password', 'Password') }} {{ Form::password('password') }} </div> <div>
240
Should we run the tests now, well see that, when the form was submitted, Codeception expected to be redirected to /admin, but was not.
1 2 3 4 5 6 7 8
Scenario Steps: 8. I see current url equals "/admin" <-- RED 7. I click "Login" 6. I fill field "password","1324" 5. I fill field "email","[email protected]" 4. I see current url equals "/login" 3. I am on page "/admin" 2. So that I perform administrative tasks
Resources
To make this test pass, we first need a new resourceful controller for managing user sessions.
1
Now, we can update the view, and specify that the form should POST to the store method of SessionsController.
1
241
This takes care of the migration and schema, but we also need a test record within this table. Laravel offers seed files for this very purpose.
1
This command will generate the following boilerplate, excluding the test record that Ive already inserted.
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php // app/database/seeds/UserTableSeeder.php class UsersTableSeeder extends Seeder { public function run() { $user = new User; $user->email = '[email protected]'; $user->password = Hash::make('1234'); $user->save(); } }
<?php // app/config/testing/database.php return array( 'connections' => array( 'mysql' => array( 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'my-test-db', 'username' => 'root', 'password' => '1234',
242
Be sure to replace the credentials above with your own. Also, Codeception needs to know these details as well. They can be added to the master codeception.yml file that should be in the root of your project.
1 2 3 4 5 6 7
modules: config: Db: dsn: 'mysql:host=localhost;dbname=TEST_DB_NAME' user: 'USERNAME' password: 'PASSWORD' dump: app/tests/_data/dump.sql
But, thats it; you now have a test database ready to go! Lets migrate and seed this new users table.
1 2 3
// app/controllers/SessionsController public function store() { $creds = [ 'email' => Input::get('email'), 'password' => Input::get('password') ]; if (Auth::attempt($creds)) return Redirect::to('admin'); }
243
Invalid Credentials
Id like to write one more test, though. Lets make sure that, if incorrect credentials are specified, then the login page is reloaded, along with an Invalid Credentials error message.
1 2 3 4 5 6 7 8 9 10
// app/tests/acceptance/LoginCest.php public function loginWithInvalidCredentials(WebGuy $I) { $I->amOnPage('/login'); $I->click('Login'); $I->seeCurrentUrlEquals('/login'); $I->see('Invalid Credentials', '.flash'); }
1) LoginCest.loginWithInvalidCredentials Guy couldn't see current url equals "/login": Failed asserting that two str\ ings are equal. --- Expected +++ Actual @@ @@ -'/login' +'/sessions' Scenario Steps: 3. I see current url equals "/login" <-- RED 2. I click "Login" 1. I am on page "/login"
This is one of those situations where we dont know exactly what the next step is. Thats the downside to writing acceptance tests. Weve proven that this piece of functionality doesnt work, but we dont have enough tests to determine precisely what to do next. Now, if we had also written some unit and functional tests, this would never be the case. Nonetheless, well continue on. Lets update the controllers store method to redirect back to the login page if authentication fails.
244
// app/controllers/SessionsController.php public function store() { $creds = [ 'email' => Input::get('email'), 'password' => Input::get('password') ]; if (Auth::attempt($creds)) return Redirect::to('admin'); return Redirect::to('login')->withInput(); }
The final step is to ensure that the Invalid Credentials message displays, as the latest running of the tests show:
1 2 3 4 5
Scenario Steps: 4. I see "Invalid Credentials",".flash" <-- RED 3. I see current url equals "/login" 2. I click "Login" 1. I am on page "/login"
In Laravel, we can flash messages when redirecting by passing a session key and value pair to the with method.
1 2 3
To capture this value from the view, we only need to verify whether the session object has a message key, and, if so, display it within a <div>.
1 2 3 4 5
And that should do it! If working along, manually check your work in the browser to prove that the login form functions as expected.
245
Tip: Once you embrace a consistent TDD cycle, youll find that, most of the time, your browser will remain closed.
Summary
Hopefully, if you worked along with this chapter, you found that, though helpful, we require more fine-grained tests to handle the situations when a scenario step fails without providing enough details. In the following chapter, well toy around with functional testing in Codeception, which offers mostly the same API, while offering a few bonuses.
The DB Module
The DB module requires a bit of setup. First, edit the global configuration file - codeception.yml - and provide your database credentials within the DB configuration block. Dont forget to update the DSN, username, and password with your credentials.
246
247
modules: config: Db: dsn: 'mysql:host=localhost;dbname=DB_NAME' user: 'USERNAME' password: 'PASSWORD' dump: app/tests/_data/dump.sql
The DB modules core responsibility is to clean up the database after each test. This way, we can ensure that each test is working with the same data set. To populate your database, Codeception requires a raw SQL dump. You can either tackle this directly from a GUI, like Sequel Pro (via an Export command ), or most systems should have a mysqldump executable available from the command line. This SQL dump should be placed within app/tests/_data/dump.sql. Using the previously mentioned executable, we can dump our database to this location in a single command:
1 2
Updating TestGuy
The final step is to re-build the TestGuy class. Any time that you add or remove a module from a suite, re-run the build command. The reason why is because files, like TestGuy, CodeGuy, and WebGuy are dynamically generated, based upon the modules that youve included.
1 2 3 4 5 6 7
codecept build /Users/Jeffrey/Desktop/codecept-chapter/app/tests/acceptance/WebGuy.php gen\ erated successfully. 39 methods added /Users/Jeffrey/Desktop/codecept-chapter/app/tests/functional/TestGuy.php ge\ nerated successfully. 52 methods added /Users/Jeffrey/Desktop/codecept-chapter/app/tests/unit/CodeGuy.php generate\ d successfully. 0 methods added
Registering a User
In the previous chapter, we wrote an acceptance test for logging a user in. This time, using functional testing, lets write a test for registering a new user. This test should walk through the process of visiting the /register page, filling out the form, and then verifying whether the new user was saved to the database. As always, we begin by generating a new test - this time, for the functional suite.
248
<?php // app/tests/functional/RegistrationCept.php $I = new TestGuy($scenario); $I->wantTo('register for a new account'); $I->lookForwardTo('be a member'); $I->amOnPage('/register'); $I->see('Register', 'h1'); $I->fillField('Email:', '[email protected]'); $I->fillField('Password:', '1234'); $I->click('Register Now'); $I->seeCurrentUrlEquals('/login'); $I->see('You may now sign in!', '.flash'); $I->seeInDatabase('users', ['email' => '[email protected]']);
Did you notice that last method, seeInDatabase? This references some of the sugar that the Db module provided. With a single line of code, we can peak into the database, and ensure that a new record was, in fact, added to the users table. Dont forget that, for each test, the database will be refreshed. This time, if we run the tests, rather than Codeception failing on a scenario step without providing any feedback, well see that an exception was thrown, along with the stack trace.
1 2
249
<!doctype html> <html> <head> <meta charset=utf-8> <title>Register</title> </head> <body> <h1>Register</h1> {{ Form::open() }} {{ Form::label('email', 'Email:') }} {{ Form::text('email') }} {{ Form::label('password', 'Password:') }} {{ Form::text('password') }} {{ Form::submit('Register Now') }} {{ Form::close() }} </body> </html>
Great! According to the tests, the only remaining thing to do is respond to the POST request, and then create the user. Forgive me for ignoring validation; lets keep things barebones for the sake of focusing as much as possible on Codeception.
250
// app/routes.php Route::post('register', function() { $user = new User; $user->email = Input::get('email'); $user->password = Input::get('password'); $user->save(); return Redirect::to('login') ->with('message', 'You may now sign in!'); });
Finally, well display the flash message to the user, and should then be done!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<body> <h1>Register</h1> @if (Session::has('message')) <div class="flash"> {{ Session::get('message') }} </div> @endif {{ Form::open() }} {{ Form::label('email', 'Email:') }} {{ Form::text('email') }} {{ Form::label('password', 'Password:') }} {{ Form::text('password') }} {{ Form::submit('Register Now') }} {{ Form::close() }} </body>
Back to green. Did you notice that we implemented this functionality without ever touching a browser? And the best part is that, for the life of this application, we will have a test that verifies whether or not the registration process works as we expect it to.
251
Back to green
Summary
The fact that this chapter is so short is a testament to how easy Codeception is to use. Tools that are built well should not require hundreds of pages of documentation. Its really quite simple: define how the user wishes to interact with their application using a readable DSL, and then write the necessary code (while still leveraging traditional unit tests) to make it pass.
Definition: Popularized by Kent Beck and Martin Fowler, and a core Extreme Programming development methodology, continuous integration is the process of frequently (even multiple times a day) merging (or integrating) your local source revisions in an attempt to avoid what we refer to as integration hell. Each integration is then verified by an automated build server, like Travis or Jenkins. This cycle can help to prevent compatibility issues.
Continuous integration assumes that you and your team leverage a version control system, like Git. Even ten years ago, terms like source control were scary things. These days, though, its a basic part of our development process. So chances are, even if you dont realize it, you may already be practicing continuous integration (mostly). 1. 2. 3. 4. Check out a copy of the source. Create a branch and perform the bug fix or addition. Hours (not days or weeks) later, when the tests are green, fetch and merge the latest changes. If no collisions occur (meaning the tests still pass), push your changes to GitHub. To repeat, only integrate when the build is successful. 5. Upon receipt, a CI server will automatically build the project again (in an environment thats as close to production as possible) and trigger all of the tests. If the tests pass, youve successfully integrated. 6. Rinse and repeat. Item number five above might be the only step (an important one) that you dont follow in your current workflow. Because there is always the possibility that you didnt update your local copy correctly, its important to have a central continuous integration server that executes automatically.
http://travis-ci.org http://jenkins-ci.org/ http://git-scm.com/
252
253
Travis CI is one such example; think of it is as the teacher who checks over your work. The ultimate goal is to have a system for detecting errors within a matter of hours, rather than days or weeks. Git, GitHub, PHPUnit (with tests), and Travis provide the perfect recipe for integrating (execuse the pun) CI into your teams daily workflow. Any individual developers work is only a few hours away from a shared project state and can be integrated back into that state in minutes. Any integration errors are found rapidly and can be fixed rapidly. - Martin Fowler One advantage to this rapid integration is that it encourages developers to break their work into small, manageable chunks. Though it may seem that continuous integration is primarily for development teams, this isnt the case. Even as a solo developer, services like Travis will prove to be quite helpful.
Hello, Travis
Imagine that you have a popular GitHub repository that others frequently contribute to. Next, imagine if, for each pull request, a service, like Travis, would notify you if their committed changes have broken your code. Pretty neat, right? But what if you want to test the package against multiple version of PHP (5.4, 5.3, etc.)? No problem: add a few characters to your configuration file and youre done. Our CI environment provides multiple runtimes (e.g. Node.js or PHP versions), data stores and so on. Because of this, hosting your project on travis-ci.org means you can effortlessly test your library or applications against multiple runtimes and data stores without even having all of them installed locally. - travis-ci.org If youre worried that Ive yet again introduced another tool to learn (when will it end ), youll be happy to know that Travis is a cinch to use: connect to GitHub, register a service hook, add a configuration file, and youre done! Three simple steps.
1. Connect
To get started, the first step is to connect your GitHub account to Travis.
http://martinfowler.com/articles/continuousIntegration.html http://about.travis-ci.org/docs/user/getting-started/
254
2. Register Hooks
Next, view your profile, and enable one of your GitHub packages that includes tests (you can create a dummy project for testing purposes). In the screenshot below, Im activating tests for my Test Helpers package that we reviewed in the testing models chapter. When checked, Travis will automatically register the necessary service hook with GitHub.
Tip: To manually register service hooks (such as for Packagist) in GitHub, visit the Settings page for one of your repositories, click Service Hooks, and scroll down to the applicable service.
255
3. Configure
As the final step, you have to tell Travis a bit about your project. How can it run your tests, if it doesnt know which language, version, and tools it was built with? Further, some applications may require a test database, setup work, and more. All of this can be declared within a travis.yml file, placed in the root of your application.
Did You Know: When creating a package with Laravels workbench, the framework will automatically generate a travis.yml file.
Heres the absolute essentials for a basic PHP project that is dependent upon Composer.
1 2 3 4 5 6 7 8 9 10 11
language: php php: - 5.4 - 5.3 before_script: - curl -s http://getcomposer.org/installer | php - php composer.phar install --dev script: phpunit
This file makes a handful of declarations: 1. 2. 3. 4. Which language are we working with? Which versions of that language must this project be tested against? Anything we need to do before running the tests? Which test script should be used?
Overall, its fairly simplistic! Remember: this is a Yaml file, so pay close attention to your indentation.
256
Configuring Travis
Upon adding this new file, commit your changes and push them to GitHub. Once GitHub registers the commit, it will fire off a notification to Travis (your service hooks at work), which, in turn, will read the projects configuration file and execute its tests. Shortly after, youll receive an email with the results. Heres an example of one I received today, notifying me that the tests all passed.
257
There will be plenty of times, though, when green isnt the color of the day. When the tests invariably fail, Travis will provide a stack trace, detailing the problem.
258
Build Configuration
As I noted earlier, Travis is a fairly straight-forward tool. Thats what makes it so useful. Having said that, there will certainly be projects that require extended configuration (within your travis.yml file). This section will outline various common options.
259
Absolute Basics
If nothing else, Travis needs to know the language and version of the project.
1 2 3
With these three lines alone, Travis will test your project with PHPUnit, against PHP version 5.4. To, say, test against version 5.2 as well, simply add one line:
1 2 3 4
Bootstrapping
When running PHPUnit, you will often want to register a bootstrap file, such as Composers vendor/autoload.php. This sort of configuration should be placed, per usual, within your phpunit.xml file. Remember: Laravel, too, will generate this file for you.
Register Dependencies
Each time that Travis runs your repos tests, it will build an environment, based upon the configuration file. As such, you must explicitly declare all dependencies or commands that should be executed before running the test script. The following snippet will download Composer and install all dependencies that are declared within your projects composer.json file.
1 2 3
Maybe you have a dedicated shell script that will install or prepare various dependencies. That, too, should be added to before_script.
1 2
before_script: - ./bin/setup-dependencies.sh
Notice that these are just simple commands. Anything that you would write in the Terminal may be used here. Perhaps you need to execute an Artisan command first:
260
before_script: - curl -s http://getcomposer.org/installer | php - php composer.phar install --dev - php artisan migrate --seed
Notifications
By default, Travis will send email notifications for each commit to the commit author and repository owner. However, this can be modified if necessary.
Set Recipients
1 2 3 4 5
IRC
Travis can even update your teams IRC channel with each commit. Lets say that Taylor wanted Travis to send through the results of each commit to Laravels IRC channel. To his configuration file, he could add:
1 2 3
Summary
Continuous integration is not Laravel specific. Nonetheless, its a pattern that all modern development teams follow in 2013. Are you? Once again, its an unfortunate truth that scary jargon like this can deter developers. Software development is confusing enough; do we really need to introduce this much confusing terminology? That said, please dont let two words keep you from sleeping better at night. Take a few hours, reread this chapter, and integrate Travis CI into your development process today.
Mock It
Lets imagine that a piece of your code uses the native file_get_contents() function. Rather than grouping it directly within a particular method, instead, extract it to its own method that you can then mock. Consider this dummy method that returns the contents of a file.
1 2 3 4
public function getContents($file) { return "The content of the given file is: " . file_get_contents($file); }
Unfortunately, this function isnt testable without a bit of trickery. Lets fix that by extracting the global function reference.
261
262
public function getContents($file) { return "The contents of the given file is: " . $this->fetch($file); } public function fetch($file) { return file_get_contents($file); }
Now that we have a wrapper for this function, getContents is a cinch to unit test.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class TheClassTest extends PHPUnit_Framework_TestCase { public function testGetContents() { $mock = Mockery::mock('TheClass')->makePartial(); $mock->shouldReceive('fetch') ->once() ->with('foo') ->andReturn('bar'); $sentence = $mock->getContents('foo'); $this->assertEquals('The contents of the given file is: bar', $sent\ ence); } }
263
In its current state, triggering the showTime() function will, as expected, return a time stamp. As no time() function exists in the current namespace, PHP will move up and assume that were referring to the global version of time(). However, we can add a custom time() function that will override the default version. In effect, were essentially create App\time().
1 2 3 4 5 6 7 8
<?php namespace App; function time() { return 'foo'; } function showTime() { return time(); }
With this modification, when calling showTime(), foo will be returned! Lets use this in our test.
1 2 3 4 5 6 7 8 9 10
<?php namespace App\Helpers; function time() { return 'foo'; } class FunctionsTest extends PHPUnit_Framework_TestCase { public function testShowTime() { $this->assertEquals('foo', showTime()); } }
Admittedly, this is a silly example. In fact, were not really testing anything! Always be careful of that when mocking. But, hopefully, the basic principle has shown through. If you cannot remove a global function but still need to test a particular method, first attempt to create a wrapper for the function that can be mocked. As a fallback, use the namespacing trick.
264
If, for some reason, the mysqldump executable isnt available to you, there are plenty of MySQL GUIs that will do the job for you. If using the popular Sequel Pro app, choose File -> Export.
265
The Route class enableFilters method should activate these filters when testing.
<?php class TwitterSearch { protected static $url = 'http://search.twitter.com?q='; protected $searchTerm; public function __construct($searchTerm) { $this->searchTerm = $searchTerm; } public function compile() { /* ... */ } public function fetch() { $url = urlencode($this->url . $this->searchTerm); return file_get_contents($url); } }
When testing this class, you may decide to stub or mock that fetch method to prevent the code from querying the Twitter API. With Mockery, this is a cinch; what you need is a partial mock.
https://github.com/padraic/mockery
266
Definition: A Partial Mocks is useful when you only need to mock a couple of the methods on an object, leaving the remainder free to respond to calls as they normally would.
This code will only mock the fetch method on the TwitterSearch class, leaving all others untouched and free to behave as they normally would when instantiated. Because of this, its important to pass in any constructor arguments in the form of an array, as the second argument. Now, when the fetch() is triggered at some point in the object, foo will be returned, leaving the Twitter API untouched.
$mock = Mockery::mock('TwitterSearch')->makePartial();
This declares that all methods on the mock object should defer to the original implementation, unless an expectation is declared. So, to reiterate, the difference in this approach is that the presence of an expectation will determine if that method is mocked, or defaults to the original implementation.
1 2 3 4 5 6
$mock = Mockery::mock('TwitterSearch')->makePartial(); $mock->fetch(); // calls real method $mock = Mockery::mock('TwitterSearch')->makePartial(); $mock->shouldReceive('fetch')->once()->andReturn('stub'); $mock->fetch(); // returns foo
Though this wont always be the case, if you find that youre leveraging partial mocks too frequently, this might be an indicator that your code should be refactored to better follow the single responsibility principle. In the case of the above example, this certainly holds true. The fetch method, which simply returns the contents of a file as a string, should be extracted to its own class. Then, TwitterSearch could merely ask for that functionality upon instantiation, allowing for an easy test double.
267
Goodbye
Well, I suppose this brings us to a close (cue the closing Saturday Night Live music). Hopefully, the book lived up to what you expected, and, just maybe, you see testing in your future. You know what? It better dang well be in your future. I worked hard on this book! The truth, though, is that this is an incredibly complex topic that consists of multiple methodologies, terminologies, frameworks, helpers, and more. No, its not a skill that you pick up in a few hours. Ive been at this for a long time, but still find myself learning new tricks and finding pitfalls every day. I hope the same will be true for you!
Testimonials: If you enjoyed the book, and wouldnt mind providing a short testimonial that will be used in various promotional spots around the web, do so in this thread. Even a sentence or two would be appreciated.
Wait a Second
But wait - this isnt the end! Were just getting started. Laravel Testing Decoded wasnt meant to be released and forgotten. Rest assured that, not only will it continue to be updated to reflect new testing functionality in the Laravel framework, but I also plan to release new chapters throughout 2013. One of the first supplementary chapters will focus on building the purchase website for this very book. This will provide plenty of real-world scenarios to dig further into acceptance and unit testing. So, with that in mind - and in closing - Ill leave you with one question: what do you want to learn next?
Stay in Touch
.
Lets be friends! Im incredibly active on Twitter, and, of course, spend much of my days making
https://github.com/JeffreyWay/Laravel-Testing-Decoded/issues/1 https://github.com/JeffreyWay/Laravel-Testing-Decoded/issues
268
Goodbye
269
Tuts+ Premium the best educational site on the web. Dont be a stranger!
http://twitter.com/jeffrey_way http://tutsplus.com