Overview

Data Access Layer (DAL) testing targets code that interacts directly with the database to verify correctness, integrity, and reliability.

  • Beyond basic CRUD operations, this testing focuses on:
    • Is data accurately stored in the database?
    • Are redundant or unnecessary data prevented from being saved?
    • Do database constraints (e.g., UNIQUE, NOT NULL) function correctly?
    • Do custom queries return the expected results precisely?
  • Tips
    • Always include basic CRUD tests, ensuring flows like create → save → retrieve → verify are covered.
    • Use H2 as a test-only database but configure constraints to mimic the production environment as closely as possible.
    • Include exception tests (e.g., shouldThrow) to improve test reliability.
    • For integration tests, use @Transactional with @Rollback to restore the database state after each test.
    • Use @DataJpaTest for slice testing at the repository layer; for full integration tests, configure additional setup.

Purpose

Verifying Data Persistence

  • Persistence refers to the property of storing data outside the application memory
    • The key of this test is to verify that an entity object, once saved to the database, maintains the same values when retrieved again.
@DisplayName("Verify data is persistently saved in DB upon product save")
class SaveProductTest {
 
    @Test
    @DisplayName("Saving a valid product persists it in the DB")
    void should_saveProduct_when_validInput() {
        // given: create product to save
        Product product = new Product("Mechanical Keyboard", 99000);
 
        // when: save product
        Product saved = productRepository.save(product);
 
        // then: verify saved product is not null and fields match
        assertThat(saved).isNotNull();
        assertThat(saved.getId()).isNotNull();
        assertThat(saved.getName()).isEqualTo("Mechanical Keyboard");
        assertThat(saved.getPrice()).isEqualTo(99000);
    }
}

Negative test case

@DisplayName("Product save failure cases")
class SaveProductFailTest {
 
    @Test
    @DisplayName("Saving a product with negative price is rejected")
    void should_throwException_when_priceIsNegative() {
        // given: invalid product with negative price
        Product invalidProduct = new Product("Faulty Item", -5000);
 
        // when & then: expect exception on save
        assertThatThrownBy(() -> productRepository.save(invalidProduct))
            .isInstanceOf(DataIntegrityViolationException.class);
    }
}

Ensuring Data Consistency

  • Consistency = logical correctness of data
  • Ex) two users cannot have the same email
@DisplayName("Exception occurs when saving duplicate emails")
class DuplicateEmailTest {
 
    @Test
    @DisplayName("Saving two users with the same email throws exception due to UNIQUE constraint")
    void should_fail_when_duplicateEmail() {
        // given: two users with the same email
        User user1 = new User("test@example.com", "Alice");
        User user2 = new User("test@example.com", "Bob");
 
        // when: save first user
        userRepository.save(user1);
 
        // then: saving second user throws exception
        assertThatThrownBy(() -> userRepository.save(user2))
            .isInstanceOf(DataIntegrityViolationException.class);
    }
}

Database Constraint validation

@DisplayName("NOT NULL Constraint Test")
@Nested
class NotNullTest {
 
    @Test
    @DisplayName("Saving fails when product name is null")
    void should_fail_when_nameIsNull() {
        // given: product created without a name
        Product product = new Product(null, 25000);
 
        // when & then: saving throws exception
        assertThatThrownBy(() -> productRepository.save(product))
            .isInstanceOf(DataIntegrityViolationException.class);
    }
}
ConstraintDescriptionException Type
NOT NULLDoes not allow null valuesDataIntegrityViolationException
UNIQUEDoes not allow duplicate valuesDataIntegrityViolationException
FOREIGN KEYEnsures referential integrity (parent ID must exist)Constra

Verifying Query Result Accuracy

  • Query result verification ensures that queries written by developers—such as JPQL, SQL, Native Query, QueryDSL—return the exact expected data.
// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
 
    /**
     * Finds products whose names contain the given keyword.
     * @param keyword the search keyword
     * @return list of products with names containing the keyword
     */
    List<Product> findByNameContaining(String keyword);
}
@DisplayName("Search products containing a specific keyword in the name")
@Nested
class FindByKeywordTest {
 
    @Test
    @DisplayName("Returns 2 products when searching for 'Mouse' after saving 2 such products")
    void should_returnProductsContainingKeyword() {
        // given: save 2 products containing 'Mouse'
        productRepository.save(new Product("Bluetooth Mouse", 25000));
        productRepository.save(new Product("Wired Mouse", 18000));
        productRepository.save(new Product("Keyboard", 30000));
 
        // when: search products with 'Mouse' in their name
        List<Product> result = productRepository.findByNameContaining("Mouse");
 
        // then: verify number of results and product names
        assertThat(result).hasSize(2)
                          .extracting(Product::getName)
                          .containsExactlyInAnyOrder("Bluetooth Mouse", "Wired Mouse");
    }
}

What should be tested?

Test ItemTesting Needed?Explanation
Queries using @Query
- Query Auto Generation
Required ✅Manually written queries are prone to typos or join errors
Method name queries with 2+ conditionsRequired ✅Auto-generated query logic may differ from intended logic
Simple method name queries (e.g., findById)
- Built in by Spring Data JPA
Not needed ❌Well-tested built-in functionality in Spring Data JPA
Single-condition field search (e.g., findByEmail)Usually not needed ❌Exceptions allowed if field is critical to business logic

@DataJpaTest

Overview

@DataJpaTest is a specialized slice test annotation in Spring Boot designed for testing the JPA Repository layer.

  • loads only the JPA-related beans selectively without loading the entire application context, enabling a fast and lightweight testing environment
  • When to Use?
    • To verify that the repository’s basic CRUD operations work correctly
    • To validate the behavior and return values of custom query methods
    • When you want to reset the database state after tests
    • To quickly test JPA-related functionality without starting the full server
  • Add @Transactional on top of the testing class (ex. UserRepositoryTest.java)

Automatically loaded components

@DataJpaTest automatically loads the following components:

ComponentRole in @DataJpaTest
@Entity classesScans for all your entity classes (e.g., Product, User) so JPA knows about them.
@Repository interfacesFinds all your Spring Data JPA repositories (e.g., ProductRepository) and creates real instances of them.
DataSourceConfigures a connection to a database. By default, it replaces your real database with a fast, in-memory H2 database.
TestEntityManagerThis is a key testing tool. It’s a simplified version of EntityManager with extra helper methods designed specifically for setting up data and making assertions in tests.
- JPA (Jakarta Persistence API)
Transaction ManagementWraps each test in a transaction that is rolled back at the end, so tests don’t interfere with each other.

Key Features Summary

FeatureDescription
@DataJpaTestLoads only JPA-related beans, resulting in faster tests
Slice TestingTests specific layers without loading the full context
Built-in @TransactionalAutomatically rolls back transactions after each test method, resetting DB state
- Transaction
Default DB: H2Uses in-memory H2 DB unless otherwise configured
@AutoConfigureTestDatabaseCan be used to replace the default DB with an actual database
TestEntityManager SupportProvides a test-friendly helper that extends JPA’s EntityManager

Test Data Preparation Strategies

StrategyDescriptionRecommended Usage
@BeforeEachDirectly create common data before each testSimple test environment setup
Fixture ClassSeparate reusable object creation methodsRepeated object setups across multiple tests
Fixture + BuilderAchieve flexibility and reusability simultaneouslyWhen various field combinations are needed

Using JUnit5’s @BeforeEach

@BeforeEach
void setUp() {
    memberRepository.deleteAll();  // Remove previous test data
    memberRepository.save(new Member("hgd@gmail.com", "Hong Gil-dong", "010-1111-2222")); // Set common test data
}
  • When only simple common data is needed
  • Quick to set up, but may cause code duplication

Reusable fixtures with fixture classes

public class MemberFixture {
    public static Member createHgd() {
        return new Member("hgd@gmail.com", "Hong Gil-dong", "010-1111-2222");
    }
 
    public static Member createLee() {
        return new Member("lee@gmail.com", "Yi Sun-shin", "010-3333-4444");
    }
}
  • A fixture is a helper class that assists in creating test objects.
  • Separating methods with meaningful names improves readability and helps eliminate duplication in tests.
  • Use when repeatedly creating objects with the same structure across multiple tests

Builder Pattern + fixture

public class MemberFixture {
    public static MemberBuilder builder() {
        return new MemberBuilder()
                .email("test@example.com")
                .name("Tester")
                .phone("010-0000-0000");
    }
}
 
memberRepository.save(
    MemberFixture.builder().name("홍길동").email("hong@example.com").build()
);

Verification Methods

  • In JPA Repository testing, verification methods mainly fall into two categories:
    1. Return Value (State) Verification: Confirm that a specific query method behaves as expected
    2. Database State Verification: Confirm that the number of stored entities and their field values are correctly persisted
StrategyDescriptionExample Usage
State VerificationCheck if return values from findByXxx() match expectations
- single-entity lookup
assertThat(optional).isPresent()
DB State VerificationCheck if saved count and field values are correctly reflected in DB
- multiple entity properties
hasSize(n), extracting("email")

Return value

@Test
void findByEmail_shouldReturnMemberIfExists() {
    // given: prepare and save a member entity
    Member member = new Member("hgd@gmail.com", "Hong Gil-dong", "010-1111-2222");
    Member savedMember = memberRepository.save(member);
 
    // when: find member by email
    Optional<Member> found = memberRepository.findByEmail("hgd@gmail.com");
 
    // then: verify presence and correct email
    assertThat(found).isPresent();  // Optional is not empty
    assertThat(found.get().getEmail()).isEqualTo("hgd@gmail.com");
}
  • Suitable for validating the correct operation of single-entity lookup methods like findByEmail().
  • The main goal is to test the behavior of JPA query methods, hence fits slice testing.

Database State

@Test
void findAll_shouldReturnAllSavedMembers() {
    // given: save two members
    memberRepository.save(new Member("kim@test.com", "Kim Cheol-soo", "010-2222-3333"));
    memberRepository.save(new Member("lee@test.com", "Lee Young-hee", "010-4444-5555"));
 
    // when: retrieve all members
    List<Member> members = memberRepository.findAll();
 
    // then: verify the count and email values
    assertThat(members).hasSize(2);
    assertThat(members)
        .extracting("email")
        .containsExactlyInAnyOrder("kim@test.com", "lee@test.com");
}
  • Checks if the number of stored entities and their field values match expectations via findAll().
  • extracting() extracts specific fields from a list of entities, making it very useful for validating multiple entity properties.

Other environments

AnnotationTarget TechnologyMain Components LoadedFocus
@DataJpaTestSpring Data JPA (JPA ORM)JPA entities, repositories, EntityManager, HibernateFull JPA persistence layer testing (entity mapping, JPQL, caching, proxies)
@JdbcTestPlain Spring JDBCJdbcTemplate, DataSource, TransactionManagerRaw JDBC query and database interaction testing
@DataJdbcTestSpring Data JDBCSpring Data JDBC repos, DataSource, TransactionManagerTesting simplified JDBC-based repositories (no ORM)

Add EnableJpaAuditing

You have to put @EnableJpaAuditing on your test class because @DataJpaTest does not automatically enable JPA Auditing.

  • @DataJpaTest is a “slice” test annotation. It’s designed to load only the necessary components for testing JPA repositories, which includes:
    • The in-memory database configuration
    • Spring Data JPA repositories.
    • JPA entities and entity manager.
  • It specifically excludes components that are not directly related to the JPA layer. JPA Auditing, which automatically populates fields like createdDate or lastModifiedDate, is a separate feature provided by Spring Data. To make it work, you need to explicitly enable it.
  • Since your application-test.yml file is for configuring things like the database URL and logging levels, it cannot enable a Spring feature like JPA Auditing, which requires a specific annotation on a configuration class or a test class.
  • Adding @EnableJpaAuditing to your @DataJpaTest class or a test configuration class is the correct way to ensure that your auditable fields are populated during repository tests.
  
@EnableJpaAuditing  
@DataJpaTest  
public class ChannelRepositoryTest {
	...
}