← Назад к вопросам

Проверял ли Unit-тесты при работе с базами данных

2.2 Middle🔥 181 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью#ORM и Hibernate

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Unit-тесты при работе с базами данных

Краткий ответ

Да, я многократно писал и проверял unit-тесты для работы с базами данных. Это критическая часть разработки, так как ошибки в коде, работающем с БД, часто приводят к потере данных и серьёзным bagам в production.

Стратегия тестирования с БД

1. Разделение на уровни тестов

Unit Tests (быстрые, изолированные):
  └─ Бизнес-логика без БД (mocks, stubs)

Integration Tests (медленнее, с реальной БД):
  └─ Работа с реальной БД
  └─ Проверка SQL запросов
  └─ Проверка транзакций

E2E Tests (самые медленные):
  └─ Полный цикл: API → БД → ответ

Подход 1: Unit-тесты без БД (Mocking)

Самый быстрый способ — использовать mock/stub для репозиториев:

public interface UserRepository {
    User findById(Long id);
    void save(User user);
    void delete(Long id);
}

public class UserService {
    private UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User getUserProfile(Long id) {
        User user = userRepository.findById(id);
        if (user == null) {
            throw new UserNotFoundException("User not found");
        }
        user.setLastAccess(LocalDateTime.now());
        return user;
    }
}
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    public void testGetUserProfile_Success() {
        Long userId = 1L;
        User expectedUser = new User(userId, "John Doe");
        when(userRepository.findById(userId)).thenReturn(expectedUser);
        
        User result = userService.getUserProfile(userId);
        
        assertNotNull(result);
        assertEquals("John Doe", result.getName());
        verify(userRepository).findById(userId);
    }
    
    @Test
    public void testGetUserProfile_NotFound() {
        Long userId = 999L;
        when(userRepository.findById(userId)).thenReturn(null);
        
        assertThrows(UserNotFoundException.class, () -> {
            userService.getUserProfile(userId);
        });
    }
}

Преимущества Unit-тестов с mocks:

  • Очень быстрые (выполняются за миллисекунды)
  • Изолированные (не зависят от БД)
  • Детерминированные (всегда одинаковый результат)
  • Легко параллелизируются

Подход 2: Integration-тесты с реальной БД

Для тестирования самих запросов к БД используем реальную БД:

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class UserRepositoryTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Test
    public void testSaveAndFindUser() {
        User user = new User();
        user.setName("John");
        user.setEmail("john@test.com");
        
        User savedUser = userRepository.save(user);
        entityManager.flush();
        
        User foundUser = userRepository.findById(savedUser.getId()).orElse(null);
        assertNotNull(foundUser);
        assertEquals("John", foundUser.getName());
    }
    
    @Test
    public void testFindByEmail() {
        User user = new User();
        user.setName("Jane");
        user.setEmail("jane@test.com");
        userRepository.save(user);
        entityManager.flush();
        entityManager.clear();
        
        User foundUser = userRepository.findByEmail("jane@test.com");
        
        assertNotNull(foundUser);
        assertEquals("Jane", foundUser.getName());
    }
    
    @Test
    public void testDeleteUser() {
        User user = new User();
        user.setName("Tom");
        user.setEmail("tom@test.com");
        User savedUser = userRepository.save(user);
        entityManager.flush();
        
        userRepository.delete(savedUser);
        entityManager.flush();
        
        Optional<User> foundUser = userRepository.findById(savedUser.getId());
        assertTrue(foundUser.isEmpty());
    }
}

Технологии для Integration-тестов:

@Testcontainers
public class UserRepositoryWithTestcontainersTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = 
        new PostgreSQLContainer<>("postgres:14")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @Test
    public void testWithRealPostgres() {
        // Тест с реальной PostgreSQL в Docker
    }
}

Подход 3: Custom Repository для проверки SQL

@Repository
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    
    @Autowired
    private EntityManager entityManager;
    
    @Override
    public List<User> findActiveUsers() {
        String jpql = "SELECT u FROM User u WHERE u.active = true ORDER BY u.name";
        return entityManager.createQuery(jpql, User.class)
            .getResultList();
    }
}

@DataJpaTest
public class UserRepositoryCustomTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    public void testFindActiveUsers() {
        User activeUser = new User("John", true);
        User inactiveUser = new User("Jane", false);
        userRepository.saveAll(Arrays.asList(activeUser, inactiveUser));
        
        List<User> activeUsers = userRepository.findActiveUsers();
        
        assertEquals(1, activeUsers.size());
        assertEquals("John", activeUsers.get(0).getName());
    }
}

Лучшие практики при тестировании БД

1. Изолируйте тесты друг от друга

@Transactional
public class UserRepositoryTest {
    // Тесты не влияют друг на друга благодаря откату
}

2. Используйте TestData builders

public class UserBuilder {
    private String name = "Default User";
    private String email = "default@test.com";
    private boolean active = true;
    
    public UserBuilder withName(String name) {
        this.name = name;
        return this;
    }
    
    public User build() {
        return new User(name, email, active);
    }
}

User user = new UserBuilder()
    .withName("John")
    .build();

3. Проверяйте ограничения целостности данных

@Test
public void testForeignKeyConstraint() {
    User user = new User();
    user.setName("John");
    user.setDepartmentId(9999L);
    
    assertThrows(DataIntegrityViolationException.class, () -> {
        userRepository.save(user);
    });
}

@Test
public void testUniqueConstraint() {
    User user1 = new User();
    user1.setEmail("duplicate@test.com");
    userRepository.save(user1);
    
    User user2 = new User();
    user2.setEmail("duplicate@test.com");
    
    assertThrows(DataIntegrityViolationException.class, () -> {
        userRepository.save(user2);
    });
}

4. Проверяйте транзакции

@Test
public void testTransactionRollback() {
    try {
        userService.createUserAndAssignToGroup(user, group);
    } catch (Exception e) {
        // При ошибке всё откатывается
    }
    
    assertFalse(userRepository.findById(user.getId()).isPresent());
}

Пример полной стратегии тестирования

  • Unit-тесты (без БД): 150 тестов, выполняются за 2 секунды
  • Integration-тесты (с БД): 40 тестов, выполняются за 30 секунд
  • E2E тесты: 10 тестов, выполняются за 2 минуты

Покрытие:

  • Бизнес-логика: 95%
  • SQL запросы: 85%
  • Весь цикл: 60%

Практические рекомендации

  • Большинство тестов должны быть unit-тесты (быстрые, без БД)
  • Integration-тесты для критичного функционала (работа с данными)
  • Используйте @Transactional для изоляции тестов
  • Избегайте использования production БД в тестах
  • Параллелизируйте выполнение unit-тестов
  • Измеряйте покрытие кода (минимум 80-90%)
  • Пишите тесты перед кодом (TDD) для лучшего дизайна
Проверял ли Unit-тесты при работе с базами данных | PrepBro