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 |