0Pricing
Testing Mastery: JUnit, Mockito & Integration Tests · 강의

데이터베이스 상호 작용 테스트

데이터 접근 계층을 위한 통합 테스트를 작성해 관계형 데이터베이스와 올바르게 상호 작용하는지 확인합니다.

데이터베이스 상호 작용 테스트은(는) CoddyKit의 무료 Testing Mastery: JUnit, Mockito & Integration Tests 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Testing Mastery: JUnit, Mockito & Integration Tests 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Testing Mastery: JUnit, Mockito & Integration Tests 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Test Database Interactions?

Database interactions are a critical part of most applications. Testing these interactions ensures your app reliably stores, retrieves, and updates data.

  • Data Integrity: Guarantees data is stored correctly.
  • Business Logic: Verifies data-related business rules.
  • Error Prevention: Catches issues before they reach production.

The Challenges of Real Databases

Testing directly with a real production database can be tricky and lead to unreliable tests:

  • Slow: Real DBs add significant time to test suites.
  • Stateful: Tests can leave leftover data, affecting subsequent tests.
  • Complex Setup: Requires a running external service and specific configurations.
  • Isolation: Difficult to ensure each test runs in an isolated environment.

In-Memory Databases to the Rescue

To overcome these challenges, we often use in-memory databases for integration tests. These are lightweight databases that run entirely within your application's memory.

  • Fast: No disk I/O, quick startup/shutdown.
  • Isolated: Each test run can start with a fresh, empty database.
  • Easy Setup: Often just a dependency and a connection URL.

Popular choices include H2, HSQLDB, and Apache Derby.

Setting Up Your H2 Database

Let's see how easy it is to connect to an H2 in-memory database using standard JDBC. It works just like a regular database, but lives in your application's memory.

You'll need the H2 dependency (e.g., Maven: com.h2database:h2).

import java.sql.*;

public class H2Demo {
  public static void main(String[] args) throws SQLException {
    String jdbcUrl = "jdbc:h2:mem:testdb";
    String username = "sa";
    String password = "";

    try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) {
      Statement stmt = conn.createStatement();
      stmt.execute("CREATE TABLE products (id INT PRIMARY KEY, name VARCHAR(255))");
      stmt.executeUpdate("INSERT INTO products (id, name) VALUES (1, 'Laptop')");

      ResultSet rs = stmt.executeQuery("SELECT * FROM products");
      if (rs.next()) {
        System.out.println("Product: " + rs.getString("name"));
      }
    }
  }
}

Introducing the Data Access Layer

Your application typically uses a Data Access Object (DAO) or Repository pattern to interact with the database. This layer abstracts away the low-level JDBC or ORM details.

We'll use a simple ProductRepository to manage Product objects.

public class Product {
  private int id;
  private String name;

  public Product(int id, String name) {
    this.id = id;
    this.name = name;
  }

  public int getId() { return id; }
  public String getName() { return name; }
}

import java.util.List;

public interface ProductRepository {
  void save(Product product);
  Product findById(int id);
  List<Product> findAll();
}

Implementing a Simple Repository

Here's a basic implementation of our ProductRepository using raw JDBC. In a real application, you might use Spring's JdbcTemplate or an ORM like Hibernate.

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

// Product class and ProductRepository interface as defined previously

public class JdbcProductRepository implements ProductRepository {
  private Connection conn;

  public JdbcProductRepository(Connection conn) {
    this.conn = conn;
    try (Statement stmt = conn.createStatement()) {
      stmt.execute("CREATE TABLE IF NOT EXISTS products (id INT PRIMARY KEY, name VARCHAR(255))");
    } catch (SQLException e) { throw new RuntimeException(e); }
  }

  @Override
  public void save(Product product) {
    String sql = "INSERT INTO products (id, name) VALUES (?, ?)";
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
      ps.setInt(1, product.getId());
      ps.setString(2, product.getName());
      ps.executeUpdate();
    } catch (SQLException e) { throw new RuntimeException(e); }
  }

  @Override
  public Product findById(int id) {
    String sql = "SELECT id, name FROM products WHERE id = ?";
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
      ps.setInt(1, id); ResultSet rs = ps.executeQuery();
      if (rs.next()) { return new Product(rs.getInt("id"), rs.getString("name")); }
    } catch (SQLException e) { throw new RuntimeException(e); }
    return null;
  }

  @Override
  public List<Product> findAll() {
    List<Product> products = new ArrayList<>();
    String sql = "SELECT id, name FROM products";
    try (Statement stmt = conn.createStatement()) {
      ResultSet rs = stmt.executeQuery(sql);
      while (rs.next()) {
        products.add(new Product(rs.getInt("id"), rs.getString("name")));
      }
    } catch (SQLException e) { throw new RuntimeException(e); }
    return products;
  }
}

Testing the Repository - Setup

Now, let's write a simple program to test our JdbcProductRepository. We'll set up an H2 in-memory database specifically for this test run.

Notice how we create a fresh database connection for our repository, ensuring isolation.

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

// Product class (simplified for mobile)
class Product {
  private int id; private String name;
  public Product(int id, String name) { this.id = id; this.name = name; }
  public int getId() { return id; }
  public String getName() { return name; }
}

// ProductRepository interface
interface ProductRepository {
  void save(Product product);
  Product findById(int id);
  List<Product> findAll();
}

// JdbcProductRepository (simplified for mobile)
class JdbcProductRepository implements ProductRepository {
  private Connection conn;
  public JdbcProductRepository(Connection conn) {
    this.conn = conn;
    try (Statement stmt = conn.createStatement()) {
      stmt.execute("CREATE TABLE IF NOT EXISTS products (id INT PRIMARY KEY, name VARCHAR(255))");
    } catch (SQLException e) { throw new RuntimeException(e); }
  }
  @Override public void save(Product product) {
    String sql = "INSERT INTO products (id, name) VALUES (?, ?)";
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
      ps.setInt(1, product.getId()); ps.setString(2, product.getName());
      ps.executeUpdate();
    } catch (SQLException e) { throw new RuntimeException(e); }
  }
  @Override public Product findById(int id) { /* ... omitted ... */ return null; }
  @Override public List<Product> findAll() { /* ... omitted ... */ return new ArrayList<>(); }
}

public class ProductRepositoryTestRunner {
  public static void main(String[] args) throws SQLException {
    String jdbcUrl = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1";
    try (Connection conn = DriverManager.getConnection(jdbcUrl, "sa", "")) {
      System.out.println("DB connection established.");
      ProductRepository repository = new JdbcProductRepository(conn);

      Product laptop = new Product(1, "Laptop");
      repository.save(laptop);
      System.out.println("Saved product: " + laptop.getName());

      Product foundProduct = repository.findById(1);
      System.out.println("Found product: " + (foundProduct != null ? foundProduct.getName() : "None"));

      List<Product> allProducts = repository.findAll();
      System.out.println("Total products: " + allProducts.size());
    }
  }
}

Asserting Database State

In a real test, simply printing isn't enough. You need to assert that the database state matches your expectations after an operation. This confirms your DAO methods work correctly.

Let's add some basic checks to verify the outcomes.

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

// Product class (simplified for mobile)
class Product {
  private int id; private String name;
  public Product(int id, String name) { this.id = id; this.name = name; }
  public int getId() { return id; }
  public String getName() { return name; }
}

// ProductRepository interface
interface ProductRepository {
  void save(Product product); Product findById(int id); List<Product> findAll();
}

// JdbcProductRepository (simplified for mobile)
class JdbcProductRepository implements ProductRepository {
  private Connection conn;
  public JdbcProductRepository(Connection conn) { /* ... omitted ... */ this.conn = conn; }
  @Override public void save(Product product) { /* ... omitted ... */ }
  @Override public Product findById(int id) {
    String sql = "SELECT id, name FROM products WHERE id = ?";
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
      ps.setInt(1, id); ResultSet rs = ps.executeQuery();
      if (rs.next()) { return new Product(rs.getInt("id"), rs.getString("name")); }
    } catch (SQLException e) { throw new RuntimeException(e); }
    return null;
  }
  @Override public List<Product> findAll() {
    List<Product> products = new ArrayList<>();
    String sql = "SELECT id, name FROM products";
    try (Statement stmt = conn.createStatement()) {
      ResultSet rs = stmt.executeQuery(sql);
      while (rs.next()) { products.add(new Product(rs.getInt("id"), rs.getString("name"))); }
    } catch (SQLException e) { throw new RuntimeException(e); }
    return products;
  }
}

public class ProductRepositoryAssertions {
  public static void main(String[] args) throws SQLException {
    String jdbcUrl = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1";
    try (Connection conn = DriverManager.getConnection(jdbcUrl, "sa", "")) {
      ProductRepository repository = new JdbcProductRepository(conn);

      Product laptop = new Product(1, "Laptop");
      repository.save(laptop);

      // Assertion 1: Check if product was saved
      Product found = repository.findById(1);
      if (found != null && found.getName().equals("Laptop")) {
        System.out.println("Test Passed: Product saved & found.");
      } else { System.out.println("Test Failed: Product not found or name mismatch."); }

      // Assertion 2: Check total count
      List<Product> all = repository.findAll();
      if (all.size() == 1) {
        System.out.println("Test Passed: Correct product count.");
      } else { System.out.println("Test Failed: Incorrect product count."); }
    }
  }
}

Ensuring Clean Tests with Transactions

For more robust test isolation, especially when using frameworks like Spring, you can leverage transactions.

By annotating your test methods with @Transactional, any changes made to the database within that test will automatically be rolled back after the test completes. This leaves the database in a clean state for the next test.

  • Automatic Cleanup: No need for manual DELETE statements.
  • Isolation: Each test runs as if it's the only one.
  • Faster: Rolling back is often quicker than deleting and re-inserting.

Database Testing Best Practices

To make your database integration tests effective and maintainable, consider these tips:

  • Use In-Memory DBs: For speed and isolation.
  • Minimal Setup: Only create tables/data necessary for the specific test.
  • Transactional Tests: Automatically roll back changes.
  • Clear Assertions: Verify expected data and state.
  • Focus on DAO: Test the data access layer, not business logic here.

Quick Check: DB Testing

Which of the following is a primary benefit of using an in-memory database like H2 for integration tests?

Recap: Testing Database Interactions

In this lesson, we learned how to effectively test database interactions.

  • We understood the challenges of testing with real databases.
  • We discovered in-memory databases like H2 as a fast and isolated solution.
  • We saw how to set up and interact with an H2 database programmatically.
  • We practiced writing tests for a Data Access Object (DAO), ensuring data integrity and correct behavior.
  • We touched upon transactional tests for automatic cleanup.

Keep practicing to ensure your application's data layer is rock solid!

자주 묻는 질문

“데이터베이스 상호 작용 테스트” 강의는 무료인가요?

네 — “데이터베이스 상호 작용 테스트” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Testing Mastery: JUnit, Mockito & Integration Tests 강의 전체를 잠금 해제할 수 있습니다. Testing Mastery: JUnit, Mockito & Integration Tests 강의에는 총 4개의 강의가 포함되어 있습니다.

“데이터베이스 상호 작용 테스트”에서 뭘 배우나요?

데이터 접근 계층을 위한 통합 테스트를 작성해 관계형 데이터베이스와 올바르게 상호 작용하는지 확인합니다. 브라우저에서 직접 실행하는 실습 코드로 Testing Mastery: JUnit, Mockito & Integration Tests을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Testing Mastery: JUnit, Mockito & Integration Tests을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Testing Mastery: JUnit, Mockito & Integration Tests은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“데이터베이스 상호 작용 테스트” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Testing Mastery: JUnit, Mockito & Integration Tests 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Testing Mastery: JUnit, Mockito & Integration Tests 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 단위 테스트와 통합 테스트 비교
  2. 통합 테스트 설정
  3. 데이터베이스 상호 작용 테스트
  4. WireMock으로 외부 API 테스트하기
← Testing Mastery: JUnit, Mockito & Integration Tests(으)로 돌아가기