diff --git a/README.md b/README.md index 9d30129..fad977c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # Sample cart project ## sample usage + ``` createRegularBasket(); - $basket->add('G01'); - $basket->add('R01'); - $basket->add('R01'); - $basket->add('R01'); + $simpleCart = new SimpleCartFacade(); + $simpleCart->add('G01'); + $simpleCart->add('R01'); + $simpleCart->add('R01'); + $simpleCart->add('R01'); - $total = $basket->total(); + $total = $simpleCart->total(); ``` ## assumptions: @@ -30,15 +31,15 @@ ## instalation 1. `docker-compose up -d` -2. inside container `composer install` +2. inside container `composer install` or simply `docker exec -it php_sample_cart bash -c "composer install"` ## run tests: - unit - - `docker exec -it php_sample_cart bash -c "vendor/bin/phpunit --configuration phpunit --testsuite 'Unit Tests'"` + - `docker exec -it php_sample_cart bash -c "vendor/bin/phpunit --configuration phpunit.xml --testsuite 'Unit Tests'"` - integration - - `docker exec -it php_sample_cart bash -c "vendor/bin/phpunit --configuration phpunit --testsuite 'Integration Tests'"` + - `docker exec -it php_sample_cart bash -c "vendor/bin/phpunit --configuration phpunit.xml --testsuite 'Integration Tests'"` - unit + integration: - `docker exec -it php_sample_cart bash -c "composer test"` - static tests (`phpstan`) diff --git a/docker-compose.yml b/docker-compose.yml index 5fb2786..1d710b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: php: build: diff --git a/phpunit.xml b/phpunit.xml index 7493d7e..bcf8a31 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,8 @@ regularBasket = $this->createBasketFactory()->createRegularBasket(); + } + + public function add(string $code): void + { + $this->regularBasket->add($code); + } + + public function total(): float + { + return $this->regularBasket->total(); + } + + private function createBasketFactory(): BasketFactory + { + return new RegularBasketFactory(); + } +} diff --git a/src/Model/BasketFactory.php b/src/Model/BasketFactory.php index 26729ae..fe02b71 100644 --- a/src/Model/BasketFactory.php +++ b/src/Model/BasketFactory.php @@ -6,36 +6,7 @@ use App\Repository\JsonProductRepository; use App\Repository\ProductRepository; -class BasketFactory +interface BasketFactory { - public function createRegularBasket(): Basket - { - return new RegularBasket( - $this->createProductCatalogueFilterable(), - $this->createShippingCostCalculator(), - $this->createDiscountOfferCalculator(), - ); - } - - private function createProductCatalogueFilterable(): ProductCatalogueFilterable - { - return new ProductCatalogue( - $this->createProductRepository(), - ); - } - - private function createProductRepository(): ProductRepository - { - return new JsonProductRepository(); - } - - private function createShippingCostCalculator(): ShippingCostCalculator - { - return new BaseShippingCostCalculator(); - } - - private function createDiscountOfferCalculator(): DiscountOfferCalculator - { - return new BaseDiscountOfferCalculator(); - } -} \ No newline at end of file + public function createRegularBasket(): Basket; +} diff --git a/src/Model/RegularBasket.php b/src/Model/RegularBasket.php index c5922a2..4b31c58 100644 --- a/src/Model/RegularBasket.php +++ b/src/Model/RegularBasket.php @@ -36,4 +36,4 @@ public function total(): float return round($total, 2); } -} \ No newline at end of file +} diff --git a/src/Model/RegularBasketFactory.php b/src/Model/RegularBasketFactory.php new file mode 100644 index 0000000..736c705 --- /dev/null +++ b/src/Model/RegularBasketFactory.php @@ -0,0 +1,41 @@ +createProductCatalogueFilterable(), + $this->createShippingCostCalculator(), + $this->createDiscountOfferCalculator(), + ); + } + + private function createProductCatalogueFilterable(): ProductCatalogueFilterable + { + return new ProductCatalogue( + $this->createProductRepository(), + ); + } + + private function createProductRepository(): ProductRepository + { + return new JsonProductRepository(); + } + + private function createShippingCostCalculator(): ShippingCostCalculator + { + return new BaseShippingCostCalculator(); + } + + private function createDiscountOfferCalculator(): DiscountOfferCalculator + { + return new BaseDiscountOfferCalculator(); + } +} diff --git a/tests/Integration/Model/RegularBasketTest.php b/tests/Integration/App/SimpleCartFacadeTest.php similarity index 86% rename from tests/Integration/Model/RegularBasketTest.php rename to tests/Integration/App/SimpleCartFacadeTest.php index eaaf89f..0646d17 100644 --- a/tests/Integration/Model/RegularBasketTest.php +++ b/tests/Integration/App/SimpleCartFacadeTest.php @@ -1,20 +1,21 @@ sut = (new BasketFactory())->createRegularBasket(); + $this->sut = new SimpleCartFacade(); parent::setUp(); } diff --git a/tests/Unit/Model/BaseDiscountOfferCalculatorTest.php b/tests/Unit/Model/BaseDiscountOfferCalculatorTest.php new file mode 100644 index 0000000..2ee5c78 --- /dev/null +++ b/tests/Unit/Model/BaseDiscountOfferCalculatorTest.php @@ -0,0 +1,57 @@ +sut = new BaseDiscountOfferCalculator(); + parent::setUp(); + } + + /** + * @param array $productList + */ + #[DataProvider('totalDataProvider')] + public function testShippingCostStrategiesProvidesCorrectValue(array $productList, float $expectedDiscount): void + { + $actualShippingCosts = $this->sut->calculate($productList); + + $this->assertEquals($expectedDiscount, $actualShippingCosts); + } + + /** + * @return array|float>> + */ + public static function totalDataProvider(): array + { + $aRedProduct = new Product(); + $aRedProduct->setCode('R01'); + $aRedProduct->setName('RedOne'); + $aRedProduct->setPrice(10.0); + + + $aGreenProduct = new Product(); + $aGreenProduct->setCode('G01'); + $aGreenProduct->setName('GreenOne'); + $aGreenProduct->setPrice(100.0); + + return [ + 'Two red products gives discount' => ['productList' => [$aRedProduct, $aRedProduct], 'expectedDiscount' => 5.0], + 'Three red products gives discount once' => ['productList' => [$aRedProduct, $aRedProduct, $aRedProduct], 'expectedDiscount' => 5.0], + 'Four red products gives discount once' => ['productList' => [$aRedProduct, $aRedProduct, $aRedProduct, $aRedProduct], 'expectedDiscount' => 10.0], + 'Two green products dont gives discount' => ['productList' => [$aGreenProduct, $aGreenProduct], 'expectedDiscount' => .0], + 'Mixed products dont gives discount' => ['productList' => [$aRedProduct, $aGreenProduct], 'expectedDiscount' => .0], + ]; + } +} diff --git a/tests/Unit/Model/BaseShippingCostCalculatorTest.php b/tests/Unit/Model/BaseShippingCostCalculatorTest.php new file mode 100644 index 0000000..3170200 --- /dev/null +++ b/tests/Unit/Model/BaseShippingCostCalculatorTest.php @@ -0,0 +1,42 @@ +sut = new BaseShippingCostCalculator(); + parent::setUp(); + } + + #[DataProvider('totalDataProvider')] + public function testShippingCostStrategiesProvidesCorrectValue(float $total, float $expectedShippingCost): void + { + $actualShippingCosts = $this->sut->calculate($total); + + $this->assertEquals($expectedShippingCost, $actualShippingCosts); + } + + /** + * @return array> + */ + public static function totalDataProvider(): array + { + return [ + 'Low shipping cost strategy is calculated' => ['total' => 10.0, 'expectedShippingCost' => 4.95], + 'Medium shipping cost strategy is calculated on boundary value' => ['total' => 50.0, 'expectedShippingCost' => 2.95], + 'Medium shipping cost strategy is calculated' => ['total' => 51.0, 'expectedShippingCost' => 2.95], + 'High shipping cost strategy is calculated on boundary value' => ['total' => 90.0, 'expectedShippingCost' => .0], + 'High shipping cost strategy is calculated' => ['total' => 91.0, 'expectedShippingCost' => .0], + ]; + } +} diff --git a/tests/Unit/Model/ProductCatalogueTest.php b/tests/Unit/Model/ProductCatalogueTest.php index 532bb76..f4007d1 100644 --- a/tests/Unit/Model/ProductCatalogueTest.php +++ b/tests/Unit/Model/ProductCatalogueTest.php @@ -1,7 +1,7 @@ productCatalogue = $this->createMock(ProductCatalogueFilterable::class); + $this->shippingCostCalculator = $this->createMock(ShippingCostCalculator::class); + $this->discountCalculator = $this->createMock(DiscountOfferCalculator::class); + + $this->sut = new RegularBasket( + $this->productCatalogue, + $this->shippingCostCalculator, + $this->discountCalculator + ); + + parent::setUp(); + } + + #[DataProvider('calculationDataProvider')] + public function testTotalsCalculation(float $productPrice, float $shippingCost, float $discount, float $expectedTotal): void + { + $aProduct = new Product(); + $aProduct->setPrice($productPrice); + + $this->productCatalogue->expects($this->once()) + ->method('getProductByCode') + ->willReturn($aProduct); + $this->discountCalculator->expects($this->once()) + ->method('calculate') + ->with([$aProduct]) + ->willReturn($discount); + $this->shippingCostCalculator->expects($this->once()) + ->method('calculate') + ->with($productPrice - $discount) + ->willReturn($shippingCost); + + $this->sut->add('SomeCode'); + + $actualTotal = $this->sut->total(); + + $this->assertEquals($expectedTotal, $actualTotal); + } + + /** + * @return array> + */ + public static function calculationDataProvider(): array + { + return [ + 'No shipping, no discount' => ['productPrice' => 100.0, 'shippingCost' => .0, 'discount' => .0, 'expectedTotal' => 100.0], + 'Shipping, no discount' => ['productPrice' => 100.0, 'shippingCost' => 10.0, 'discount' => .0, 'expectedTotal' => 110.0], + 'No shipping, Discount' => ['productPrice' => 100.0, 'shippingCost' => .0, 'discount' => 10.0, 'expectedTotal' => 90.0], + 'Shipping, Discount' => ['productPrice' => 100.0, 'shippingCost' => 10.0, 'discount' => 5.0, 'expectedTotal' => 105.0], + ]; + } + + public function testAddToCartIsTriggeringCatalogue(): void + { + $this->productCatalogue->expects($this->exactly(3)) + ->method('getProductByCode') + ->willReturn(new Product()); + + $this->sut->add('R01'); + $this->sut->add('G01'); + $this->sut->add('G01'); + + $sutReflection = new ReflectionClass($this->sut); + $property = $sutReflection->getProperty('items'); + $actualItems = (array)$property->getValue($this->sut); + + $this->assertEquals(3, count($actualItems)); + } +}