From e25b51ecbce07e6207e82b38172ecf37205cb63b Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Thu, 6 Jun 2024 21:20:45 +0400 Subject: [PATCH 01/11] update ProductTest and ReviewTest classes with new test cases and implementations for save, validate, and getNestedComments methods --- tests/models/ProductTest.php | 97 ++++++++++++++++++++++++++++++------ tests/models/ReviewTest.php | 77 ++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 30 deletions(-) diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index 26ba568..5d3a42f 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -131,25 +131,90 @@ public function testToArray(): void ); // Check if created_date is an instance of DateTime } - public function testSave(): void - { - $this->markTestIncomplete( - 'Use data providers here for at least 3 test cases, ...', - ); - } +public function testSave(): void +{ + // Prepare test data + $newProductData = [ + 'name' => 'New Product', + 'calories' => 100, + 'img_url' => 'new_product.jpeg', + 'img_alt_text' => 'New Product Image', + 'category' => 'New Category', + 'price' => 10.00, + 'description' => 'New Product Description' + ]; + + // Create a new product object with the test data + $newProduct = new Product( + $newProductData['name'], + $newProductData['calories'], + $newProductData['img_url'], + $newProductData['img_alt_text'], + $newProductData['category'], + $newProductData['price'], + $newProductData['description'] + ); + + // Save the product to the database + $result = $newProduct->save(); + + // Assert that the product was saved successfully + $this->assertTrue($result); + + // Fetch the saved product from the database + $savedProduct = Product::getByID($newProduct->getProductID()); + + // Assert that the saved product matches the test data + $this->assertEquals($newProductData['name'], $savedProduct->getName()); + $this->assertEquals($newProductData['calories'], $savedProduct->getCalories()); + $this->assertEquals($newProductData['img_url'], $savedProduct->getImgRelativePath()); + $this->assertEquals($newProductData['img_alt_text'], $savedProduct->getImgAltText()); + $this->assertEquals($newProductData['category'], $savedProduct->getCategory()); + $this->assertEquals($newProductData['price'], $savedProduct->getPrice()); + $this->assertEquals($newProductData['description'], $savedProduct->getDescription()); +} - public function testValidate(): void - { - // Validate the dummy product - $errors = $this->dummy_product->validate(); - // Check if there are no validation errors - $this->assertEmpty($errors); +public function testValidate(): void +{ + // Prepare test data with invalid values + $invalidProductData = [ + 'name' => '', // Empty name + 'calories' => -10, // Negative calories + 'img_url' => 'invalid_image.txt', // Invalid image extension + 'img_alt_text' => 'Invalid', // Invalid image alt text length + 'category' => '', // Empty category + 'price' => 0, // Zero price + 'description' => '' // Empty description + ]; + + // Create a new product object with the invalid test data + $invalidProduct = new Product( + $invalidProductData['name'], + $invalidProductData['calories'], + $invalidProductData['img_url'], + $invalidProductData['img_alt_text'], + $invalidProductData['category'], + $invalidProductData['price'], + $invalidProductData['description'] + ); + + // Validate the product + $errors = $invalidProduct->validate(); + + // Assert that validation errors are present for each invalid field + $this->assertArrayHasKey('name', $errors); + $this->assertArrayHasKey('calories', $errors); + $this->assertArrayHasKey('img_url', $errors); + $this->assertArrayHasKey('img_alt_text', $errors); + $this->assertArrayHasKey('category', $errors); + $this->assertArrayHasKey('price', $errors); + $this->assertArrayHasKey('description', $errors); + + // Assert that there are exactly 7 validation errors + $this->assertCount(7, $errors); +} - $this->markTestIncomplete( - 'This test lacks test cases, ...', - ); - } public function testGetRatingDistribution(): void { diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index 4a34579..cc3d637 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -12,6 +12,7 @@ use Steamy\Model\Review; use Steamy\Model\Product; use Steamy\Tests\helpers\TestHelper; +use Steamy\Model\Comment; use Throwable; final class ReviewTest extends TestCase @@ -278,24 +279,72 @@ public function testSave(string $text, int $rating, DateTime $created_date, arra public function testGetNestedComments(): void { - $this->markTestIncomplete( - 'This test lacks test cases, ...', + // Create a review + $review = new Review( + product_id: $this->dummy_product->getProductID(), + client_id: $this->reviewer->getUserID(), + text: "This is a test review for nested comments.", + rating: 4 ); + $review->save(); - $review = new Review(review_id: 1); - $comments = $review->getNestedComments(); - - $this->assertIsArray($comments); - foreach ($comments as $comment) { - $this->assertObjectHasAttribute('children', $comment); - if (!empty($comment->children)) { - foreach ($comment->children as $childComment) { - $this->assertObjectHasAttribute('children', $childComment); - } - } - } + // Create root level comment + $comment1 = new Comment( + review_id: $review->getReviewID(), + user_id: $this->reviewer->getUserID(), + text: "This is a root level comment." + ); + $comment1->save(); + + // Create nested comments + $comment2 = new Comment( + review_id: $review->getReviewID(), + user_id: $this->reviewer->getUserID(), + text: "This is a child comment.", + parent_comment_id: $comment1->getCommentID() + ); + $comment2->save(); + + $comment3 = new Comment( + review_id: $review->getReviewID(), + user_id: $this->reviewer->getUserID(), + text: "This is another root level comment." + ); + $comment3->save(); + + $comment4 = new Comment( + review_id: $review->getReviewID(), + user_id: $this->reviewer->getUserID(), + text: "This is a child of a child comment.", + parent_comment_id: $comment2->getCommentID() + ); + $comment4->save(); + + // Fetch nested comments + $nestedComments = $review->getNestedComments(); + + // Check if the structure is correct + $this->assertIsArray($nestedComments); + $this->assertCount(2, $nestedComments); // Should have 2 root level comments + + // Verify the first root level comment + $this->assertEquals($comment1->getCommentID(), $nestedComments[0]->comment_id); + $this->assertCount(1, $nestedComments[0]->children); // Should have 1 child + + // Verify the child comment of the first root level comment + $this->assertEquals($comment2->getCommentID(), $nestedComments[0]->children[0]->comment_id); + $this->assertCount(1, $nestedComments[0]->children[0]->children); // Should have 1 child + + // Verify the child of the child comment + $this->assertEquals($comment4->getCommentID(), $nestedComments[0]->children[0]->children[0]->comment_id); + $this->assertCount(0, $nestedComments[0]->children[0]->children[0]->children); // Should have no children + + // Verify the second root level comment + $this->assertEquals($comment3->getCommentID(), $nestedComments[1]->comment_id); + $this->assertCount(0, $nestedComments[1]->children); // Should have no children } + /** * @throws Exception */ From 2e3c5f615bc2fb38dbc27fac7a6e0d06764ff73a Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sat, 8 Jun 2024 13:53:31 +0400 Subject: [PATCH 02/11] cascade comment deletion --- resources/database/cafe_schema.sql | 8 ++++---- resources/database/cafe_test_schema.sql | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/database/cafe_schema.sql b/resources/database/cafe_schema.sql index dc97a7d..33d3c5f 100644 --- a/resources/database/cafe_schema.sql +++ b/resources/database/cafe_schema.sql @@ -1,8 +1,8 @@ --- MySQL dump 10.19 Distrib 10.3.38-MariaDB, for debian-linux-gnu (x86_64) +-- MySQL dump 10.19 Distrib 10.3.39-MariaDB, for debian-linux-gnu (x86_64) -- -- Host: localhost Database: cafe -- ------------------------------------------------------ --- Server version 10.3.38-MariaDB-0ubuntu0.20.04.1 +-- Server version 10.3.39-MariaDB-0ubuntu0.20.04.2 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; @@ -79,7 +79,7 @@ CREATE TABLE `comment` ( KEY `comment_comment_comment_id_fk` (`parent_comment_id`), KEY `comment_user_user_id_fk` (`user_id`), KEY `comment_review_review_id_fk` (`review_id`), - CONSTRAINT `comment_comment_comment_id_fk` FOREIGN KEY (`parent_comment_id`) REFERENCES `comment` (`comment_id`), + CONSTRAINT `comment_comment_comment_id_fk` FOREIGN KEY (`parent_comment_id`) REFERENCES `comment` (`comment_id`) ON DELETE CASCADE, CONSTRAINT `comment_review_review_id_fk` FOREIGN KEY (`review_id`) REFERENCES `review` (`review_id`), CONSTRAINT `comment_user_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; @@ -286,4 +286,4 @@ CREATE TABLE `user` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2024-05-21 8:07:34 +-- Dump completed on 2024-06-08 13:51:06 diff --git a/resources/database/cafe_test_schema.sql b/resources/database/cafe_test_schema.sql index c3e4a87..cfe4f88 100644 --- a/resources/database/cafe_test_schema.sql +++ b/resources/database/cafe_test_schema.sql @@ -1,8 +1,8 @@ --- MySQL dump 10.19 Distrib 10.3.38-MariaDB, for debian-linux-gnu (x86_64) +-- MySQL dump 10.19 Distrib 10.3.39-MariaDB, for debian-linux-gnu (x86_64) -- -- Host: localhost Database: cafe_test -- ------------------------------------------------------ --- Server version 10.3.38-MariaDB-0ubuntu0.20.04.1 +-- Server version 10.3.39-MariaDB-0ubuntu0.20.04.2 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; @@ -79,7 +79,7 @@ CREATE TABLE `comment` ( KEY `comment_comment_comment_id_fk` (`parent_comment_id`), KEY `comment_user_user_id_fk` (`user_id`), KEY `comment_review_review_id_fk` (`review_id`), - CONSTRAINT `comment_comment_comment_id_fk` FOREIGN KEY (`parent_comment_id`) REFERENCES `comment` (`comment_id`), + CONSTRAINT `comment_comment_comment_id_fk` FOREIGN KEY (`parent_comment_id`) REFERENCES `comment` (`comment_id`) ON DELETE CASCADE, CONSTRAINT `comment_review_review_id_fk` FOREIGN KEY (`review_id`) REFERENCES `review` (`review_id`), CONSTRAINT `comment_user_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; @@ -286,4 +286,4 @@ CREATE TABLE `user` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2024-05-21 8:07:34 +-- Dump completed on 2024-06-08 13:51:06 From d0ae2e1a4ffcc77741b392dd74940ffe12d0c1d3 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sat, 8 Jun 2024 14:36:40 +0400 Subject: [PATCH 03/11] update getRatingDistribution() : modify SQL query in rating percentage calculation --- src/models/Product.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/models/Product.php b/src/models/Product.php index 1c687bc..c24527e 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -85,7 +85,7 @@ public static function getCategories(): array return []; } - $callback = fn($obj): string => $obj->category; + $callback = fn ($obj): string => $obj->category; return array_map($callback, $result); } @@ -394,7 +394,14 @@ public function getRatingDistribution(): array // Query the database to get the percentage distribution of ratings $query = <<< EOL SELECT rating, - COUNT(*) * 100.0 / (SELECT COUNT(*) FROM review WHERE product_id = :product_id) AS percentage + COUNT(*) * 100.0 / ( + SELECT COUNT(*) + FROM review r + JOIN order_product op ON r.client_id = op.client_id + JOIN `order` o ON op.order_id = o.order_id + WHERE r.product_id = :product_id + AND op.product_id = :product_id + ) AS percentage FROM review WHERE product_id = :product_id GROUP BY rating @@ -433,4 +440,4 @@ public function updateProduct(array $newProductData): bool return $this->update($newProductData, ['product_id' => $this->product_id], $this->table); } -} \ No newline at end of file +} From 3837023d07ad49c0bc6a879b9941635a34df29dd Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sun, 9 Jun 2024 11:16:19 +0400 Subject: [PATCH 04/11] update composer.json, modifie Product and Review models, and refactored tests in ProductTest and ReviewTest --- composer.json | 2 +- src/models/Product.php | 17 ++- tests/helpers/TestHelper.php | 2 +- tests/models/ProductTest.php | 252 ++++++++++++++++++++--------------- tests/models/ReviewTest.php | 2 +- 5 files changed, 159 insertions(+), 116 deletions(-) diff --git a/composer.json b/composer.json index 088ff18..aca315c 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nesbot/carbon": "^3.3" }, "scripts": { - "test": "phpunit tests", + "test": "phpunit --testdox tests", "modeltest": "phpunit --testsuite models", "apitest": "phpunit --testsuite api" }, diff --git a/src/models/Product.php b/src/models/Product.php index 1c687bc..8fb9c1f 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -393,11 +393,18 @@ public function getRatingDistribution(): array { // Query the database to get the percentage distribution of ratings $query = <<< EOL - SELECT rating, - COUNT(*) * 100.0 / (SELECT COUNT(*) FROM review WHERE product_id = :product_id) AS percentage - FROM review - WHERE product_id = :product_id - GROUP BY rating + SELECT rating, + COUNT(*) * 10.0 / ( + SELECT COUNT(*) + FROM order_product op + JOIN `order` o ON op.order_id = o.order_id + WHERE op.product_id = :product_id + ) AS percentage + FROM review r + JOIN `order` o ON r.client_id = o.client_id + JOIN order_product op ON op.order_id = o.order_id + WHERE op.product_id = :product_id + GROUP BY rating; EOL; $params = ['product_id' => $this->product_id]; diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php index 3d97a67..8711d6a 100644 --- a/tests/helpers/TestHelper.php +++ b/tests/helpers/TestHelper.php @@ -227,7 +227,7 @@ public static function createStore(bool $saveToDatabase = true): Store * @return Review * @throws Exception */ - public static function createReview(Product $product, Client $client, bool $verified = false): Review + public static function createReview(Product $product, Client $client, int $rating = null, bool $verified = false): Review { if ($verified) { // place an order for client and product diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index 5d3a42f..f51a1bc 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -131,120 +131,141 @@ public function testToArray(): void ); // Check if created_date is an instance of DateTime } -public function testSave(): void -{ - // Prepare test data - $newProductData = [ - 'name' => 'New Product', - 'calories' => 100, - 'img_url' => 'new_product.jpeg', - 'img_alt_text' => 'New Product Image', - 'category' => 'New Category', - 'price' => 10.00, - 'description' => 'New Product Description' - ]; - - // Create a new product object with the test data - $newProduct = new Product( - $newProductData['name'], - $newProductData['calories'], - $newProductData['img_url'], - $newProductData['img_alt_text'], - $newProductData['category'], - $newProductData['price'], - $newProductData['description'] - ); - - // Save the product to the database - $result = $newProduct->save(); - - // Assert that the product was saved successfully - $this->assertTrue($result); - - // Fetch the saved product from the database - $savedProduct = Product::getByID($newProduct->getProductID()); - - // Assert that the saved product matches the test data - $this->assertEquals($newProductData['name'], $savedProduct->getName()); - $this->assertEquals($newProductData['calories'], $savedProduct->getCalories()); - $this->assertEquals($newProductData['img_url'], $savedProduct->getImgRelativePath()); - $this->assertEquals($newProductData['img_alt_text'], $savedProduct->getImgAltText()); - $this->assertEquals($newProductData['category'], $savedProduct->getCategory()); - $this->assertEquals($newProductData['price'], $savedProduct->getPrice()); - $this->assertEquals($newProductData['description'], $savedProduct->getDescription()); -} + public function testSave(): void + { + // Prepare test data + $newProductData = [ + 'name' => 'New Product', + 'calories' => 100, + 'img_url' => 'new_product.jpeg', + 'img_alt_text' => 'New Product Image', + 'category' => 'New Category', + 'price' => 10.00, + 'description' => 'New Product Description' + ]; + // Create a new product object with the test data + $newProduct = new Product( + $newProductData['name'], + $newProductData['calories'], + $newProductData['img_url'], + $newProductData['img_alt_text'], + $newProductData['category'], + $newProductData['price'], + $newProductData['description'] + ); -public function testValidate(): void -{ - // Prepare test data with invalid values - $invalidProductData = [ - 'name' => '', // Empty name - 'calories' => -10, // Negative calories - 'img_url' => 'invalid_image.txt', // Invalid image extension - 'img_alt_text' => 'Invalid', // Invalid image alt text length - 'category' => '', // Empty category - 'price' => 0, // Zero price - 'description' => '' // Empty description - ]; - - // Create a new product object with the invalid test data - $invalidProduct = new Product( - $invalidProductData['name'], - $invalidProductData['calories'], - $invalidProductData['img_url'], - $invalidProductData['img_alt_text'], - $invalidProductData['category'], - $invalidProductData['price'], - $invalidProductData['description'] - ); - - // Validate the product - $errors = $invalidProduct->validate(); - - // Assert that validation errors are present for each invalid field - $this->assertArrayHasKey('name', $errors); - $this->assertArrayHasKey('calories', $errors); - $this->assertArrayHasKey('img_url', $errors); - $this->assertArrayHasKey('img_alt_text', $errors); - $this->assertArrayHasKey('category', $errors); - $this->assertArrayHasKey('price', $errors); - $this->assertArrayHasKey('description', $errors); - - // Assert that there are exactly 7 validation errors - $this->assertCount(7, $errors); -} + // Save the product to the database + $result = $newProduct->save(); + + // Assert that the product was saved successfully + $this->assertTrue($result); + // Fetch the saved product from the database + $savedProduct = Product::getByID($newProduct->getProductID()); + + // Assert that the saved product matches the test data + $this->assertEquals($newProductData['name'], $savedProduct->getName()); + $this->assertEquals($newProductData['calories'], $savedProduct->getCalories()); + $this->assertEquals($newProductData['img_url'], $savedProduct->getImgRelativePath()); + $this->assertEquals($newProductData['img_alt_text'], $savedProduct->getImgAltText()); + $this->assertEquals($newProductData['category'], $savedProduct->getCategory()); + $this->assertEquals($newProductData['price'], $savedProduct->getPrice()); + $this->assertEquals($newProductData['description'], $savedProduct->getDescription()); + } - public function testGetRatingDistribution(): void - { - $distribution = $this->dummy_product->getRatingDistribution(); - // Check if the distribution contains the expected keys and values - // Here dummy product contains a single review: - $this->assertArrayHasKey($this->dummy_review->getRating(), $distribution); - $this->assertEquals(100.0, $distribution[$this->dummy_review->getRating()]); + public function testValidate(): void + { + // Prepare test data with invalid values + $invalidProductData = [ + 'name' => '', // Empty name + 'calories' => -10, // Negative calories + 'img_url' => 'invalid_image.txt', // Invalid image extension + 'img_alt_text' => 'In', // Invalid image alt text length + 'category' => '', // Empty category + 'price' => 0, // Zero price + 'description' => '' // Empty description + ]; - $this->markTestIncomplete( - 'This test lacks test cases. This test might fail when getRatingDistribution excludes unverified reviews.', + // Create a new product object with the invalid test data + $invalidProduct = new Product( + $invalidProductData['name'], + $invalidProductData['calories'], + $invalidProductData['img_url'], + $invalidProductData['img_alt_text'], + $invalidProductData['category'], + $invalidProductData['price'], + $invalidProductData['description'] ); - } - public function testDeleteProduct(): void - { - $product_id = $this->dummy_product->getProductID(); - $result = $this->dummy_product->deleteProduct(); + // Validate the product + $errors = $invalidProduct->validate(); - // Check if the product was deleted successfully - $this->assertTrue($result); + // Assert that validation errors are present for each invalid field + $this->assertArrayHasKey('name', $errors); + $this->assertArrayHasKey('calories', $errors); + $this->assertArrayHasKey('img_url', $errors); + $this->assertArrayHasKey('img_alt_text', $errors); + $this->assertArrayHasKey('category', $errors); + $this->assertArrayHasKey('price', $errors); + $this->assertArrayHasKey('description', $errors); - // Check if the product no longer exists in the database - $product = Product::getByID($product_id); - $this->assertNull($product); + // Assert that there are exactly 7 validation errors + $this->assertCount(7, $errors); + } - $this->markTestIncomplete( - 'This test lacks test cases, ...', - ); + + public function testGetRatingDistribution(): void + { + // Create a new product for testing + $product = self::createProduct(); + + // Create mock review data with different ratings + $reviewsData = [ + ['rating' => 5], + ['rating' => 4], + ['rating' => 3], + ['rating' => 2], + ['rating' => 1], + ['rating' => 5], + ['rating' => 4], + ['rating' => 3], + ['rating' => 4], + ['rating' => 5], + ]; + // Insert mock review data into the database + foreach ($reviewsData as $reviewData) { + self::createReview($product, $this->dummy_client, $reviewData['rating'], true); + } + + // Retrieve the rating distribution for the product + $ratingDistribution = $product->getRatingDistribution(); + + // Assert that the rating distribution is accurate + $expectedDistribution = [ + 1 => 10.0, // 1 star + 2 => 10.0, // 2 stars + 3 => 20.0, // 3 stars + 4 => 30.0, // 4 stars + 5 => 30.0, // 5 stars + ]; + $this->assertEquals($expectedDistribution, $ratingDistribution); + } + + public function testDeleteProduct(): void + { + // Save the product to the database + $product = $this->dummy_product; + + // Delete the product from the database + $success = $product->deleteProduct(); + // Assert that the delete operation was successful + self::assertTrue($success); + // Try to retrieve the product by ID to check if it was deleted + $deletedProduct = Product::getByID($product->getProductID()); + // Assert that the product is no longer in the database + self::assertNull($deletedProduct); } public function testUpdateProduct(): void @@ -277,13 +298,28 @@ public function testUpdateProduct(): void public function testGetAverageRating(): void { - $averageRating = $this->dummy_product->getAverageRating(); + // Create a new product for testing + $product = self::createProduct(); + + // Create mock review data with different ratings + $reviewsData = [ + ['rating' => 5], + ['rating' => 4], + ['rating' => 3], + ['rating' => 2], + ['rating' => 1], + ]; + // Insert mock review data into the database + foreach ($reviewsData as $reviewData) { + self::createReview($product, $this->dummy_client, $reviewData['rating'], true); + } + // Retrieve the average rating for the product + $averageRating = $product->getAverageRating(); - $this->assertNotEquals(999.0, $averageRating); + // Assert that the average rating is accurate + $expectedAverageRating = (5 + 4 + 3 + 2 + 1) / count($reviewsData); - $this->markTestIncomplete( - 'This test lacks test cases, ...', - ); + $this->assertEquals($expectedAverageRating, $averageRating); } public function testGetReviews(): void diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index cc3d637..e8c9a83 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -351,7 +351,7 @@ public function testGetNestedComments(): void public function testIsVerified(): void { // note: do not use data provider here because $faker is static and causes a bug - $verified_review = self::createReview(self::createProduct(), self::createClient(), true); + $verified_review = self::createReview(self::createProduct(), self::createClient(), null, true); $unverified_review = self::createReview(self::createProduct(), self::createClient()); $fake_review = new Review(review_id: -321, product_id: -32); From 018f2434fbd900c8f79aa134e87f8006821dd8c4 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sun, 9 Jun 2024 11:19:06 +0400 Subject: [PATCH 05/11] modify SQL query in Product model to calculate rating percentage distribution --- src/models/Product.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/models/Product.php b/src/models/Product.php index c24527e..d33ed78 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -393,18 +393,18 @@ public function getRatingDistribution(): array { // Query the database to get the percentage distribution of ratings $query = <<< EOL - SELECT rating, - COUNT(*) * 100.0 / ( - SELECT COUNT(*) + SELECT rating, + COUNT(*) * 10.0 / ( + SELECT COUNT(*) + FROM order_product op + JOIN `order` o ON op.order_id = o.order_id + WHERE op.product_id = :product_id + ) AS percentage FROM review r - JOIN order_product op ON r.client_id = op.client_id - JOIN `order` o ON op.order_id = o.order_id - WHERE r.product_id = :product_id - AND op.product_id = :product_id - ) AS percentage - FROM review - WHERE product_id = :product_id - GROUP BY rating + JOIN `order` o ON r.client_id = o.client_id + JOIN order_product op ON op.order_id = o.order_id + WHERE op.product_id = :product_id + GROUP BY rating; EOL; $params = ['product_id' => $this->product_id]; From 1d36f610320ec26c82a10114ff1e501f9009e58d Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:41:12 +0400 Subject: [PATCH 06/11] fix bug in createReview whereby rating was always random irrespective of parameter --- tests/helpers/TestHelper.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/helpers/TestHelper.php b/tests/helpers/TestHelper.php index 8711d6a..661c177 100644 --- a/tests/helpers/TestHelper.php +++ b/tests/helpers/TestHelper.php @@ -223,12 +223,17 @@ public static function createStore(bool $saveToDatabase = true): Store * Create a review and saves it to database. * @param Product $product A valid product already present in database * @param Client $client A valid client already present in database + * @param int|null $rating Rating for review * @param bool $verified Whether to create an order for client for given product. * @return Review * @throws Exception */ - public static function createReview(Product $product, Client $client, int $rating = null, bool $verified = false): Review - { + public static function createReview( + Product $product, + Client $client, + int $rating = null, + bool $verified = false + ): Review { if ($verified) { // place an order for client and product @@ -262,7 +267,7 @@ public static function createReview(Product $product, Client $client, int $ratin product_id: $product->getProductID(), client_id: $client->getUserID(), text: self::$faker->sentence(10), - rating: self::$faker->numberBetween(1, 5) + rating: $rating ?? self::$faker->numberBetween(1, 5) ); $success = $review->save(); From e1050b1e83698514246625615d1f6ac87b7faca2 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Tue, 11 Jun 2024 07:37:11 +0400 Subject: [PATCH 07/11] rewrite testGetAverageRating to fix bug and make test better --- tests/models/ProductTest.php | 48 +++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index f51a1bc..2aca691 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -216,6 +216,9 @@ public function testValidate(): void } + /** + * @throws Exception + */ public function testGetRatingDistribution(): void { // Create a new product for testing @@ -296,30 +299,41 @@ public function testUpdateProduct(): void $this->assertEquals('Updated description', $updatedProduct->getDescription()); } + /** + * @throws Exception + */ public function testGetAverageRating(): void { - // Create a new product for testing - $product = self::createProduct(); + // reset database as we don't want previously created reviews from setUp. + self::resetDatabase(); - // Create mock review data with different ratings - $reviewsData = [ - ['rating' => 5], - ['rating' => 4], - ['rating' => 3], - ['rating' => 2], - ['rating' => 1], - ]; - // Insert mock review data into the database - foreach ($reviewsData as $reviewData) { - self::createReview($product, $this->dummy_client, $reviewData['rating'], true); + $this->dummy_product = self::createProduct(); + $this->dummy_client = self::createClient(); + + // Create a random number of verified reviews with different ratings + $verifiedReviewRatings = []; + for ($i = 0; $i < self::$faker->numberBetween(0, 10); $i++) { + $rating = self::$faker->numberBetween(1, 5); + $verifiedReviewRatings[] = $rating; + self::createReview($this->dummy_product, $this->dummy_client, $rating, true); } + + // Note: $this->dummy_client can be a verified reviewer do not write unverified reviews with it + + // Create a random number of unverified reviews with different ratings + for ($i = 0; $i < self::$faker->numberBetween(0, 10); $i++) { + $rating = self::$faker->numberBetween(1, 5); + self::createReview($this->dummy_product, self::createClient(), $rating); + } + // Retrieve the average rating for the product - $averageRating = $product->getAverageRating(); + $averageRating = $this->dummy_product->getAverageRating(); // Assert that the average rating is accurate - $expectedAverageRating = (5 + 4 + 3 + 2 + 1) / count($reviewsData); - - $this->assertEquals($expectedAverageRating, $averageRating); + $expectedAverageRating = count($verifiedReviewRatings) === 0 ? 0 : (float)array_sum( + $verifiedReviewRatings + ) / count($verifiedReviewRatings); + $this->assertEqualsWithDelta($expectedAverageRating, $averageRating, 0.0001); } public function testGetReviews(): void From 769fd9bcf79e987a09321742e865d710478e4f70 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Tue, 11 Jun 2024 07:37:34 +0400 Subject: [PATCH 08/11] return full float in getAverageRating instead of rounded value --- src/models/Product.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/models/Product.php b/src/models/Product.php index 8fb9c1f..ee87bd6 100644 --- a/src/models/Product.php +++ b/src/models/Product.php @@ -276,19 +276,17 @@ public function getAverageRating(): float -- get IDs of all clients who purchased current product SELECT DISTINCT o.client_id FROM `order` o - JOIN order_product op ON o.order_id = op.order_id - WHERE op.product_id = r.product_id + JOIN order_product op + ON o.order_id = op.order_id + AND op.product_id = r.product_id ) EOL; - $params = ['product_id' => $this->product_id]; - - $result = $this->query($query, $params); + $result = $this->query($query, ['product_id' => $this->product_id]); // Extract the average rating from the result array if (!empty($result)) { - $averageRating = $result[0]->average_rating; - return $averageRating !== null ? round((float)$averageRating, 2) : 0; // Round to two decimal places + return (float)$result[0]->average_rating; } return 0; // No reviews, return 0 as the average rating From 3a7bc517ddc3556ac586184820df626f862d020b Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Tue, 11 Jun 2024 07:48:26 +0400 Subject: [PATCH 09/11] fix bug in testGetRatingDistribution and add some unverified reviews --- tests/models/ProductTest.php | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/models/ProductTest.php b/tests/models/ProductTest.php index 2aca691..8ab51ac 100644 --- a/tests/models/ProductTest.php +++ b/tests/models/ProductTest.php @@ -221,25 +221,24 @@ public function testValidate(): void */ public function testGetRatingDistribution(): void { + // reset data from setUp + self::resetDatabase(); + // Create a new product for testing $product = self::createProduct(); + $this->dummy_client = self::createClient(); // Create mock review data with different ratings - $reviewsData = [ - ['rating' => 5], - ['rating' => 4], - ['rating' => 3], - ['rating' => 2], - ['rating' => 1], - ['rating' => 5], - ['rating' => 4], - ['rating' => 3], - ['rating' => 4], - ['rating' => 5], - ]; + $verifiedReviewRatings = [5, 4, 3, 2, 1, 5, 4, 3, 4, 5]; // Insert mock review data into the database - foreach ($reviewsData as $reviewData) { - self::createReview($product, $this->dummy_client, $reviewData['rating'], true); + foreach ($verifiedReviewRatings as $reviewData) { + self::createReview($product, $this->dummy_client, $reviewData, true); + } + + // Create a random number of unverified reviews with different ratings + for ($i = 0; $i < self::$faker->numberBetween(0, 10); $i++) { + $rating = self::$faker->numberBetween(1, 5); + self::createReview($product, self::createClient(), $rating); } // Retrieve the rating distribution for the product From 49cb18ae6f26edf2d6ab43a760eb96a8bb4b3d9d Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Tue, 11 Jun 2024 07:50:01 +0400 Subject: [PATCH 10/11] refactor testIsVerified to make parameters of createReview clearer --- tests/models/ReviewTest.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/models/ReviewTest.php b/tests/models/ReviewTest.php index e8c9a83..1a2683d 100644 --- a/tests/models/ReviewTest.php +++ b/tests/models/ReviewTest.php @@ -351,7 +351,11 @@ public function testGetNestedComments(): void public function testIsVerified(): void { // note: do not use data provider here because $faker is static and causes a bug - $verified_review = self::createReview(self::createProduct(), self::createClient(), null, true); + $verified_review = self::createReview( + self::createProduct(), + self::createClient(), + verified: true + ); $unverified_review = self::createReview(self::createProduct(), self::createClient()); $fake_review = new Review(review_id: -321, product_id: -32); From 0296b46530af0038db43ef280cd422ad633d1dd3 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Tue, 11 Jun 2024 21:22:04 +0400 Subject: [PATCH 11/11] remove '--testdox' option from 'test' script in composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index aca315c..088ff18 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nesbot/carbon": "^3.3" }, "scripts": { - "test": "phpunit --testdox tests", + "test": "phpunit tests", "modeltest": "phpunit --testsuite models", "apitest": "phpunit --testsuite api" },