انتقل إلى المحتوى الرئيسي

TP9.2 - اختبارات تكامل المستودعات

أهداف العمل التطبيقي​

في هذا العمل التطبيقي، سوف تقوم بـ:

  1. ✅ اختبار مستودع باستخدام @DataJpaTest
  2. ✅ استخدام قاعدة بيانات H2 حقيقية
  3. ✅ اختبار الاستعلامات المخصصة
  4. ✅ التحقق من قيود التفرد
  5. ✅ اختبار العلاقات بين الكيانات

المدة المقدرة: 1 ساعة و30 دقيقة


السياق​

لديك كيانات Product وCategory مع علاقة Many-to-One. يجب عليك اختبار أن استعلامات JPA الخاصة بك تعمل بشكل صحيح مع قاعدة بيانات حقيقية.


الجزء 1: الكيانات​

Category​

@Entity
@Table(name = "categories")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String name;

private String description;
}

Product​

@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

@Column(unique = true)
private String sku;

private BigDecimal price;
private Integer stock;
private boolean active = true;

@ManyToOne
@JoinColumn(name = "category_id")
private Category category;

@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
}

الجزء 2: المستودعات​

CategoryRepository​

public interface CategoryRepository extends JpaRepository<Category, Long> {
Optional<Category> findByName(String name);
boolean existsByName(String name);
}

ProductRepository​

public interface ProductRepository extends JpaRepository<Product, Long> {

// استعلامات حسب الخاصية البسيطة
List<Product> findByActiveTrue();
List<Product> findByActiveFalse();
Optional<Product> findBySku(String sku);

// استعلامات مع معايير
List<Product> findByPriceLessThan(BigDecimal price);
List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
List<Product> findByStockGreaterThan(Integer stock);

// استعلامات مع علاقة
List<Product> findByCategory(Category category);
List<Product> findByCategoryName(String categoryName);

// استعلامات مع ترتيب
List<Product> findByActiveTrueOrderByPriceAsc();
List<Product> findByActiveTrueOrderByCreatedAtDesc();

// استعلامات @Query
@Query("SELECT p FROM Product p WHERE p.price < :price AND p.active = true")
List<Product> findCheapActiveProducts(@Param("price") BigDecimal price);

@Query("SELECT COUNT(p) FROM Product p WHERE p.category.id = :categoryId")
long countByCategory(@Param("categoryId") Long categoryId);

@Query("SELECT p FROM Product p WHERE p.stock < :threshold")
List<Product> findLowStockProducts(@Param("threshold") Integer threshold);
}

الجزء 3: إعدادات الاختبار​

application-test.properties​

# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

الجزء 4: الاختبارات التي يجب تنفيذها​

الهيكل الأساسي​

@DataJpaTest
@ActiveProfiles("test")
class ProductRepositoryTest {

@Autowired
private ProductRepository productRepository;

@Autowired
private CategoryRepository categoryRepository;

@Autowired
private TestEntityManager entityManager;

private Category electronics;
private Category clothing;

@BeforeEach
void setUp() {
// إنشاء الفئات
electronics = categoryRepository.save(new Category(null, "Electronics", "Electronic devices"));
clothing = categoryRepository.save(new Category(null, "Clothing", "Apparel"));

// إنشاء المنتجات
entityManager.persist(new Product(null, "Laptop", "LAP001",
new BigDecimal("999.99"), 10, true, electronics, LocalDateTime.now()));
entityManager.persist(new Product(null, "Phone", "PHO001",
new BigDecimal("599.99"), 25, true, electronics, LocalDateTime.now()));
entityManager.persist(new Product(null, "T-Shirt", "TSH001",
new BigDecimal("29.99"), 100, true, clothing, LocalDateTime.now()));
entityManager.persist(new Product(null, "Old Phone", "PHO002",
new BigDecimal("199.99"), 0, false, electronics, LocalDateTime.now()));

entityManager.flush();
}
}

اختبار 1: findAll​

@Test
@DisplayName("findAll devrait retourner tous les produits")
void findAll_ShouldReturnAllProducts() {
List<Product> products = productRepository.findAll();

assertThat(products).hasSize(4);
}

اختبار 2: findByActiveTrue​

@Test
@DisplayName("findByActiveTrue devrait retourner uniquement les produits actifs")
void findByActiveTrue_ShouldReturnOnlyActiveProducts() {
// TODO: Implémenter
// Attendre 3 produits actifs
}

اختبار 3: findBySku​

@Test
@DisplayName("findBySku devrait retourner le produit correspondant")
void findBySku_WhenExists_ShouldReturnProduct() {
// TODO: Implémenter
}

@Test
@DisplayName("findBySku devrait retourner empty quand SKU inexistant")
void findBySku_WhenNotExists_ShouldReturnEmpty() {
// TODO: Implémenter
}

اختبار 4: findByPriceLessThan​

@Test
@DisplayName("findByPriceLessThan devrait retourner les produits moins chers")
void findByPriceLessThan_ShouldReturnCheaperProducts() {
List<Product> products = productRepository.findByPriceLessThan(new BigDecimal("500"));

assertThat(products)
.hasSize(2)
.extracting(Product::getName)
.containsExactlyInAnyOrder("T-Shirt", "Old Phone");
}

اختبار 5: findByCategory​

@Test
@DisplayName("findByCategory devrait retourner les produits de la catégorie")
void findByCategory_ShouldReturnProductsInCategory() {
// TODO: Implémenter
// Attendre 3 produits Electronics
}

اختبار 6: findByCategoryName​

@Test
@DisplayName("findByCategoryName devrait retourner les produits par nom de catégorie")
void findByCategoryName_ShouldReturnProducts() {
// TODO: Implémenter
}

اختبار 7: استعلام @Query مخصص​

@Test
@DisplayName("findCheapActiveProducts devrait retourner les produits actifs bon marché")
void findCheapActiveProducts_ShouldReturnCheapAndActive() {
List<Product> products = productRepository.findCheapActiveProducts(new BigDecimal("100"));

assertThat(products)
.hasSize(1)
.first()
.extracting(Product::getName)
.isEqualTo("T-Shirt");
}

اختبار 8: countByCategory​

@Test
@DisplayName("countByCategory devrait compter les produits de la catégorie")
void countByCategory_ShouldReturnCorrectCount() {
// TODO: Implémenter
}

اختبار 9: findLowStockProducts​

@Test
@DisplayName("findLowStockProducts devrait retourner les produits avec stock faible")
void findLowStockProducts_ShouldReturnLowStock() {
// TODO: Implémenter avec threshold = 5
}

اختبار 10: اختبار قيد التفرد​

@Test
@DisplayName("Devrait lever une exception pour un SKU dupliqué")
void save_WithDuplicateSku_ShouldThrowException() {
Product duplicate = new Product();
duplicate.setName("Duplicate");
duplicate.setSku("LAP001"); // SKU déjà utilisé
duplicate.setPrice(new BigDecimal("100"));

assertThrows(DataIntegrityViolationException.class, () -> {
productRepository.save(duplicate);
entityManager.flush(); // Force la persistence
});
}

اختبار 11: اختبار الحفظ مع العلاقة​

@Test
@DisplayName("Devrait sauvegarder un produit avec sa catégorie")
void save_WithCategory_ShouldPersistRelation() {
Product newProduct = new Product();
newProduct.setName("New Product");
newProduct.setSku("NEW001");
newProduct.setPrice(new BigDecimal("49.99"));
newProduct.setStock(50);
newProduct.setActive(true);
newProduct.setCategory(clothing);

Product saved = productRepository.save(newProduct);
entityManager.flush();
entityManager.clear(); // Vider le cache

Product found = productRepository.findById(saved.getId()).orElseThrow();

assertThat(found.getCategory()).isNotNull();
assertThat(found.getCategory().getName()).isEqualTo("Clothing");
}

اختبار 12: اختبار الترتيب​

@Test
@DisplayName("findByActiveTrueOrderByPriceAsc devrait trier par prix croissant")
void findByActiveTrueOrderByPriceAsc_ShouldSortByPrice() {
List<Product> products = productRepository.findByActiveTrueOrderByPriceAsc();

assertThat(products)
.extracting(Product::getPrice)
.isSorted();
}

الجزء 5: الحل​

Cliquer pour voir le solutionnaire
@DataJpaTest
@ActiveProfiles("test")
class ProductRepositoryTest {

@Autowired
private ProductRepository productRepository;

@Autowired
private CategoryRepository categoryRepository;

@Autowired
private TestEntityManager entityManager;

private Category electronics;
private Category clothing;

@BeforeEach
void setUp() {
electronics = categoryRepository.save(new Category(null, "Electronics", "Electronic devices"));
clothing = categoryRepository.save(new Category(null, "Clothing", "Apparel"));

entityManager.persist(createProduct("Laptop", "LAP001", "999.99", 10, true, electronics));
entityManager.persist(createProduct("Phone", "PHO001", "599.99", 25, true, electronics));
entityManager.persist(createProduct("T-Shirt", "TSH001", "29.99", 100, true, clothing));
entityManager.persist(createProduct("Old Phone", "PHO002", "199.99", 0, false, electronics));
entityManager.flush();
}

private Product createProduct(String name, String sku, String price, int stock, boolean active, Category cat) {
Product p = new Product();
p.setName(name);
p.setSku(sku);
p.setPrice(new BigDecimal(price));
p.setStock(stock);
p.setActive(active);
p.setCategory(cat);
p.setCreatedAt(LocalDateTime.now());
return p;
}

@Test
void findByActiveTrue_ShouldReturnOnlyActiveProducts() {
List<Product> products = productRepository.findByActiveTrue();
assertThat(products).hasSize(3);
assertThat(products).allMatch(Product::isActive);
}

@Test
void findBySku_WhenExists_ShouldReturnProduct() {
Optional<Product> product = productRepository.findBySku("LAP001");
assertThat(product).isPresent();
assertThat(product.get().getName()).isEqualTo("Laptop");
}

@Test
void findByCategory_ShouldReturnProductsInCategory() {
List<Product> products = productRepository.findByCategory(electronics);
assertThat(products).hasSize(3);
}

@Test
void findByCategoryName_ShouldReturnProducts() {
List<Product> products = productRepository.findByCategoryName("Clothing");
assertThat(products).hasSize(1);
assertThat(products.get(0).getName()).isEqualTo("T-Shirt");
}

@Test
void countByCategory_ShouldReturnCorrectCount() {
long count = productRepository.countByCategory(electronics.getId());
assertThat(count).isEqualTo(3);
}

@Test
void findLowStockProducts_ShouldReturnLowStock() {
List<Product> products = productRepository.findLowStockProducts(5);
assertThat(products).hasSize(1);
assertThat(products.get(0).getStock()).isEqualTo(0);
}
}

التحقق​

mvn test -Dtest=ProductRepositoryTest

قائمة التحقق​

  • جميع الاختبارات تمر
  • استخدام @DataJpaTest (ليس @SpringBootTest)
  • استخدام TestEntityManager للإعداد
  • اختبار الاستعلامات المخصصة
  • التحقق من القيود

🎯 الخطوة التالية​

➡️ TP9.3: اختبارات REST API


⬅️ العودة إلى الوحدة 9