Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
| 1 | <?php |
||
| 18 | class JSONCatalogTest extends \PHPUnit_Framework_TestCase |
||
| 19 | { |
||
| 20 | /** |
||
| 21 | * If the JSONCatalog is empty no products should be returned. |
||
| 22 | * |
||
| 23 | * @test |
||
| 24 | */ |
||
| 25 | public function getCatalog_JSONIsEmpty_NoProductsAreReturned() |
||
| 26 | { |
||
| 27 | // arrange |
||
| 28 | $filesystem = new Filesystem(new MemoryAdapter()); |
||
| 29 | $catalogMapperMock = $this->getMockBuilder(CatalogMapper::class)->disableOriginalConstructor()->getMock(); |
||
| 30 | /** @var $catalogMapperMock CatalogMapper A mock for the CatalogMapper class */ |
||
| 31 | $jsonCatalog = new JSONCatalogProvider($filesystem, "catalog.json", $catalogMapperMock); |
||
| 32 | |||
| 33 | // act |
||
| 34 | $catalog = $jsonCatalog->getCatalog(); |
||
| 35 | |||
| 36 | // assert |
||
| 37 | $this->assertEmpty($catalog, "getCatalog should not have returned a catalog if the catalog JSON is empty"); |
||
| 38 | } |
||
| 39 | |||
| 40 | /** |
||
| 41 | * If the filesystem read fails a CatalogException should be thrown |
||
| 42 | * |
||
| 43 | * @test |
||
| 44 | * @expectedException \Wambo\Catalog\Error\CatalogException |
||
| 45 | */ |
||
| 46 | public function getCatalog_FilesystemReadFails_CatalogExceptionIsThrown() |
||
| 47 | { |
||
| 48 | // arrange |
||
| 49 | $filesystemMock = $this->getMockBuilder(Filesystem::class)->disableOriginalConstructor()->getMock(); |
||
| 50 | $filesystemMock->method("read")->willReturn(false); |
||
| 51 | |||
| 52 | $catalogMapperMock = $this->getMockBuilder(CatalogMapper::class)->disableOriginalConstructor()->getMock(); |
||
| 53 | /** @var Filesystem $filesystemMock */ |
||
| 54 | /** @var CatalogMapper $catalogMapperMock A mock for the CatalogMapper class */ |
||
| 55 | $jsonCatalog = new JSONCatalogProvider($filesystemMock, "catalog.json", $catalogMapperMock); |
||
| 56 | |||
| 57 | // act |
||
| 58 | $jsonCatalog->getCatalog(); |
||
| 59 | } |
||
| 60 | |||
| 61 | /** |
||
| 62 | * If the CatalogMapper returns a catalog, the provider should return that catalog. |
||
| 63 | * |
||
| 64 | * @test |
||
| 65 | * @expectedException \Wambo\Catalog\Error\CatalogException |
||
| 66 | */ |
||
| 67 | public function getCatalog_JSONIsInvalid_CatalogExceptionIsThrown() |
||
| 68 | { |
||
| 69 | // arrange |
||
| 70 | $filesystem = new Filesystem(new MemoryAdapter()); |
||
| 71 | $catalogMapperMock = $this->getMockBuilder(CatalogMapper::class)->disableOriginalConstructor()->getMock(); |
||
| 72 | $catalogMapperMock->method("getCatalog")->willReturn(new Catalog(array())); |
||
| 73 | /** @var $catalogMapperMock CatalogMapper A mock for the CatalogMapper class */ |
||
| 74 | |||
| 75 | $catalogJSON = <<<JSON |
||
| 76 | [ |
||
| 77 | {},,],, |
||
| 78 | ] |
||
| 79 | JSON; |
||
| 80 | $filesystem->write("catalog.json", $catalogJSON); |
||
| 81 | $jsonCatalogProvider = new JSONCatalogProvider($filesystem, "catalog.json", $catalogMapperMock); |
||
| 82 | |||
| 83 | // act |
||
| 84 | $jsonCatalogProvider->getCatalog(); |
||
| 85 | } |
||
| 86 | |||
| 87 | /** |
||
| 88 | * If the CatalogMapper throws an exception, the provider should throw a CatalogException. |
||
| 89 | * |
||
| 90 | * @test |
||
| 91 | * @expectedException Wambo\Catalog\Error\CatalogException |
||
| 92 | */ |
||
| 93 | public function getCatalog_JSONIsValid_MapperThrowsException_CatalogExceptionIsThrown() |
||
| 94 | { |
||
| 95 | // arrange |
||
| 96 | $filesystem = new Filesystem(new MemoryAdapter()); |
||
| 97 | $catalogMapperMock = $this->getMockBuilder(CatalogMapper::class)->disableOriginalConstructor()->getMock(); |
||
| 98 | $catalogMapperMock->method("getCatalog")->willThrowException(new Exception("Some mapping error")); |
||
| 99 | /** @var $catalogMapperMock CatalogMapper A mock for the CatalogMapper class */ |
||
| 100 | |||
| 101 | $catalogJSON = <<<JSON |
||
| 102 | [] |
||
| 103 | JSON; |
||
| 104 | $filesystem->write("catalog.json", $catalogJSON); |
||
| 105 | $jsonCatalogProvider = new JSONCatalogProvider($filesystem, "catalog.json", $catalogMapperMock); |
||
| 106 | |||
| 107 | // act |
||
| 108 | $jsonCatalogProvider->getCatalog(); |
||
| 109 | } |
||
| 110 | |||
| 111 | /** |
||
| 112 | * If the CatalogMapper returns a catalog, the provider should return that catalog. |
||
| 113 | * |
||
| 114 | * @test |
||
| 115 | */ |
||
| 116 | public function getCatalog_JSONIsValid_MapperReturnsCatalog_CatalogIsReturned() |
||
| 117 | { |
||
| 118 | // arrange |
||
| 119 | $filesystem = new Filesystem(new MemoryAdapter()); |
||
| 120 | $catalogMapperMock = $this->getMockBuilder(CatalogMapper::class)->disableOriginalConstructor()->getMock(); |
||
| 121 | $catalogMapperMock->method("getCatalog")->willReturn(new Catalog(array())); |
||
| 122 | /** @var $catalogMapperMock CatalogMapper A mock for the CatalogMapper class */ |
||
| 123 | |||
| 124 | $catalogJSON = <<<JSON |
||
| 125 | [ |
||
| 126 | {} |
||
| 127 | ] |
||
| 128 | JSON; |
||
| 129 | $filesystem->write("catalog.json", $catalogJSON); |
||
| 130 | $jsonCatalogProvider = new JSONCatalogProvider($filesystem, "catalog.json", $catalogMapperMock); |
||
| 131 | |||
| 132 | // act |
||
| 133 | $catalog = $jsonCatalogProvider->getCatalog(); |
||
| 134 | |||
| 135 | // assert |
||
| 136 | $this->assertNotNull($catalog, "getCatalog should return a catalog if the mapper returned one"); |
||
| 137 | } |
||
| 138 | |||
| 139 | /** |
||
| 140 | * Integration test with local filesystem |
||
| 141 | * |
||
| 142 | * @test |
||
| 143 | */ |
||
| 144 | public function getCatalog_IntegrationTest_LocalSampleCatalogFile() |
||
| 145 | { |
||
| 146 | // arrange |
||
| 147 | $sampleCatalogFilename = "sample-catalog.json"; |
||
| 148 | $testResourceFolderPath = realpath(__DIR__ . '/resources'); |
||
| 149 | $adapter = new Local($testResourceFolderPath); |
||
| 150 | $filesystem = new Filesystem($adapter); |
||
| 151 | |||
| 152 | $contentMapper = new ContentMapper(); |
||
| 153 | $productMapper = new ProductMapper($contentMapper); |
||
| 154 | $catalogMapper = new CatalogMapper($productMapper); |
||
| 155 | |||
| 156 | $jsonCatalogProvider = new JSONCatalogProvider($filesystem, $sampleCatalogFilename, $catalogMapper); |
||
| 157 | |||
| 158 | // act |
||
| 159 | $catalog = $jsonCatalogProvider->getCatalog(); |
||
| 160 | |||
| 161 | // assert |
||
| 162 | foreach ($catalog->getProducts() as $product) { |
||
| 163 | /** @var Product $product */ |
||
| 164 | $this->assertNotEmpty($product->getSku(), "The SKU should not be empty"); |
||
| 165 | $this->assertNotEmpty($product->getSlug(), "The slug should not be empty"); |
||
| 166 | $this->assertNotEmpty($product->getTitle(), "The title should not be empty"); |
||
| 167 | $this->assertNotEmpty($product->getSummaryText(), "The summary should not be empty"); |
||
| 168 | $this->assertNotEmpty($product->getProductDescription(), "The product description should not be empty"); |
||
| 169 | } |
||
| 170 | } |
||
| 171 | |||
| 172 | /** |
||
| 173 | * Integration test |
||
| 174 | * |
||
| 175 | * @test |
||
| 176 | */ |
||
| 177 | public function getCatalog_IntegrationTest_CatalogWithProductsIsReturned() |
||
| 178 | { |
||
| 179 | // arrange |
||
| 180 | $filesystem = new Filesystem(new MemoryAdapter()); |
||
| 181 | $contentMapper = new ContentMapper(); |
||
| 182 | $productMapper = new ProductMapper($contentMapper); |
||
| 183 | $catalogMapper = new CatalogMapper($productMapper); |
||
| 184 | |||
| 185 | $catalogJSON = <<<JSON |
||
| 186 | [ |
||
| 187 | { |
||
| 188 | "sku": "t-shirt-no-1", |
||
| 189 | "slug": "t-shirt-no-1", |
||
| 190 | "title": "T-Shirt No. 1", |
||
| 191 | "summary": "Our T-Shirt No. 1", |
||
| 192 | "description": "Our fancy T-Shirt No. 1 is ..." |
||
| 193 | }, |
||
| 194 | { |
||
| 195 | "sku": "t-shirt-no-2", |
||
| 196 | "slug": "t-shirt-no-2", |
||
| 197 | "title": "T-Shirt No. 2", |
||
| 198 | "summary": "Our T-Shirt No. 2", |
||
| 199 | "description": "Our fancy T-Shirt No. 2 is ..." |
||
| 200 | }, |
||
| 201 | { |
||
| 202 | "sku": "t-shirt-no-3", |
||
| 203 | "slug": "t-shirt-no-3", |
||
| 204 | "title": "T-Shirt No. 3", |
||
| 205 | "summary": "Our T-Shirt No. 3", |
||
| 206 | "description": "Our fancy T-Shirt No. 3 is ..." |
||
| 207 | } |
||
| 208 | ] |
||
| 209 | JSON; |
||
| 210 | $filesystem->write("catalog.json", $catalogJSON); |
||
| 211 | $jsonCatalogProvider = new JSONCatalogProvider($filesystem, "catalog.json", $catalogMapper); |
||
| 212 | |||
| 213 | // act |
||
| 214 | $catalog = $jsonCatalogProvider->getCatalog(); |
||
| 215 | |||
| 216 | // assert |
||
| 217 | $this->assertCount(3, $catalog, "getCatalog should return a catalog with 3 products"); |
||
| 218 | } |
||
| 219 | |||
| 220 | /** |
||
| 221 | * Integration test: invalid JSON |
||
| 222 | * |
||
| 223 | * @test |
||
| 224 | * @dataProvider getInvalidCatalogJSON |
||
| 225 | * |
||
| 226 | * @param string $json |
||
| 227 | * |
||
| 228 | * @expectedException Wambo\Catalog\Error\CatalogException |
||
| 229 | * @expectedExceptionMessageRegExp /Unable to read catalog/ |
||
| 230 | */ |
||
| 231 | public function getCatalog_IntegrationTest_ValidCatalog_CatalogExceptionIsThrown($json) |
||
| 232 | { |
||
| 233 | // arrange |
||
| 234 | $filesystem = new Filesystem(new MemoryAdapter()); |
||
| 235 | $contentMapper = new ContentMapper(); |
||
| 236 | $productMapper = new ProductMapper($contentMapper); |
||
| 237 | $catalogMapper = new CatalogMapper($productMapper); |
||
| 238 | |||
| 239 | $filesystem->write("catalog.json", $json); |
||
| 240 | $jsonCatalogProvider = new JSONCatalogProvider($filesystem, "catalog.json", $catalogMapper); |
||
| 241 | |||
| 242 | // act |
||
| 243 | $jsonCatalogProvider->getCatalog(); |
||
| 244 | } |
||
| 245 | |||
| 246 | /** |
||
| 247 | * Integration test: valid catalog |
||
| 248 | * |
||
| 249 | * @test |
||
| 250 | */ |
||
| 251 | public function getCatalog_IntegrationTest_ValidCatalog_AllProductAttributesAreSet() |
||
| 252 | { |
||
| 253 | // arrange |
||
| 254 | $filesystem = new Filesystem(new MemoryAdapter()); |
||
| 255 | $contentMapper = new ContentMapper(); |
||
| 256 | $productMapper = new ProductMapper($contentMapper); |
||
| 257 | $catalogMapper = new CatalogMapper($productMapper); |
||
| 258 | |||
| 259 | $catalogJSON = <<<JSON |
||
| 260 | [ |
||
| 261 | { |
||
| 262 | "sku": "t-shirt-no-1", |
||
| 263 | "slug": "t-shirt-no-1-slug", |
||
| 264 | "title": "T-Shirt No. 1", |
||
| 265 | "summary": "Our T-Shirt No. 1", |
||
| 266 | "description": "Our fancy T-Shirt No. 1 is ..." |
||
| 267 | } |
||
| 268 | ] |
||
| 269 | JSON; |
||
| 270 | $filesystem->write("catalog.json", $catalogJSON); |
||
| 271 | $jsonCatalogProvider = new JSONCatalogProvider($filesystem, "catalog.json", $catalogMapper); |
||
| 272 | |||
| 273 | // act |
||
| 274 | $catalog = $jsonCatalogProvider->getCatalog(); |
||
| 275 | |||
| 276 | // assert |
||
| 277 | $products = $catalog->getProducts(); |
||
| 278 | $this->assertCount(1, $products, "getCatalog should return a catalog with one product"); |
||
| 279 | |||
| 280 | /** @var Product $firstProduct */ |
||
| 281 | $firstProduct = $products[0]; |
||
| 282 | $this->assertEquals("t-shirt-no-1", $firstProduct->getSku(), "Wrong SKU"); |
||
| 283 | $this->assertEquals("t-shirt-no-1-slug", $firstProduct->getSlug(), "Wrong slug"); |
||
| 284 | $this->assertEquals("T-Shirt No. 1", $firstProduct->getTitle(), "Wrong title"); |
||
| 285 | $this->assertEquals("Our T-Shirt No. 1", $firstProduct->getSummaryText(), "Wrong summary"); |
||
| 286 | $this->assertEquals("Our fancy T-Shirt No. 1 is ...", $firstProduct->getProductDescription(), |
||
| 287 | "Wrong description"); |
||
| 288 | } |
||
| 289 | |||
| 290 | /** |
||
| 291 | * Get invalid catalog JSON for testing |
||
| 292 | * |
||
| 293 | * @return array |
||
| 294 | */ |
||
| 295 | public static function getInvalidCatalogJSON() |
||
| 296 | { |
||
| 297 | return array( |
||
| 298 | |||
| 299 | // JavaScript instead of JSON |
||
| 300 | [ |
||
| 301 | <<<JSON |
||
| 302 | var test = { |
||
| 303 | "sku": "t-shirt-no-1", |
||
| 304 | "slug": "t-shirt-no-1-slug", |
||
| 305 | "title": "T-Shirt No. 1", |
||
| 306 | "summary": "Our T-Shirt No. 1", |
||
| 307 | "description": "Our fancy T-Shirt No. 1 is ..." |
||
| 308 | }; |
||
| 309 | JSON |
||
| 310 | ], |
||
| 311 | |||
| 312 | // Missing attribute quotes |
||
| 313 | [ |
||
| 314 | <<<JSON |
||
| 315 | [ |
||
| 316 | { |
||
| 317 | sku: "t-shirt-no-1", |
||
| 318 | slug: "t-shirt-no-1-slug", |
||
| 319 | title: "T-Shirt No. 1", |
||
| 320 | summary: "Our T-Shirt No. 1", |
||
| 321 | description: "Our fancy T-Shirt No. 1 is ..." |
||
| 322 | } |
||
| 323 | ] |
||
| 324 | JSON |
||
| 325 | ], |
||
| 326 | |||
| 327 | // Missing comma |
||
| 328 | [ |
||
| 329 | <<<JSON |
||
| 330 | [ |
||
| 331 | { |
||
| 332 | "sku": "t-shirt-no-1" |
||
| 333 | "slug": "t-shirt-no-1-slug" |
||
| 334 | "title": "T-Shirt No. 1" |
||
| 335 | "summary": "Our T-Shirt No. 1" |
||
| 336 | "description": "Our fancy T-Shirt No. 1 is ..." |
||
| 337 | } |
||
| 338 | ] |
||
| 359 |