การทดสอบเว็บด้วยข้อมูลหลายชุด โดยใช้ Selenium WebDriver + JUnit Jupiter Parameterized Test + CSV

#บทนำ

ในการทำ UI Automation Testing เรามักพบว่าขั้นตอนการทดสอบเหมือนเดิม แต่ข้อมูลที่ใช้ทดสอบแตกต่างกัน เช่น

  • ชื่อผู้ใช้หลายรูปแบบ
  • ค่าขอบเขตต่ำสุด/สูงสุด
  • ข้อมูลภาษาไทย/อังกฤษ
  • ข้อมูลปกติและข้อมูลผิดรูปแบบ
  • ข้อมูลหลายชุดจาก CSV, JSON, Excel หรือ Database

หากเราเขียน Test Case แยกสำหรับข้อมูลทุกชุด จะเกิดโค้ดซ้ำจำนวนมาก

Data-Driven Testing (DDT) คือแนวทางที่แยก Test Logic ออกจาก Test Data แล้วนำข้อมูลแต่ละชุดมารันกับ Test Logic เดิมซ้ำ ๆ

แนวคิดโดยย่อ:

Test Data
   │
   ├── Row 1
   ├── Row 2
   ├── Row 3
   └── Row N
        │
        ▼
Parameterized Test
        │
        ▼
Selenium WebDriver
        │
        ▼
Browser
        │
        ▼
Assertion / Test Result

บทความนี้ใช้:

  • Java 17+
  • Maven
  • Selenium Java 4.49.0
  • JUnit Jupiter 6.1.3
  • Google Chrome
  • Maven Surefire Plugin 3.6.0

หมายเหตุ: Selenium รุ่นปัจจุบันมี Selenium Manager รวมมาให้แล้ว โดย Selenium bindings จะเรียกใช้อัตโนมัติเพื่อจัดการ browser driver ดังนั้นโดยทั่วไปไม่จำเป็นต้องดาวน์โหลด chromedriver ด้วยตนเอง


#1. Data-Driven Testing คืออะไร

สมมติเราต้องทดสอบฟอร์มหนึ่งชุดด้วยข้อมูลต่อไปนี้

Test Data Expected Result
Selenium Received!
Data Driven Testing Received!
NPRU Received!
0123456789 Received!

ถ้าเขียนแบบปกติ อาจต้องสร้างหลาย @Test

@Test
void testSelenium() {
    // steps...
}

@Test
void testDataDrivenTesting() {
    // steps...
}

@Test
void testNpru() {
    // steps...
}

ปัญหาคือ Test Logic ซ้ำกัน

DDT จะเปลี่ยนเป็นแนวคิดนี้

@ParameterizedTest
void testForm(String input, String expected) {
    // Test Logic เขียนเพียงครั้งเดียว
}

จากนั้นให้ Framework ส่งข้อมูลแต่ละชุดเข้ามาเอง


#2. ข้อดีของ Data-Driven Testing

#ลด Code Duplication

Test Steps เขียนครั้งเดียว แต่รันได้กับข้อมูลจำนวนมาก

#เพิ่ม Test Coverage

เพิ่ม Test Case ได้โดยเพิ่มข้อมูล ไม่จำเป็นต้องเพิ่ม Test Method ใหม่

#แยก Test Data ออกจาก Test Logic

ทีม QA สามารถแก้ CSV ได้โดยไม่ต้องแก้ WebDriver flow

#เหมาะกับ Boundary Testing

ตัวอย่าง:

0
1
99
100
101

#รองรับ Positive / Negative Testing

ตัวอย่าง:

valid@example.com
invalid-email
(empty)
very-long-email

#3. เครื่องมือที่ใช้

ตรวจสอบ Java

java --version

ตรวจสอบ Maven

mvn --version

ควรมี Google Chrome ติดตั้งอยู่ในเครื่อง


#4. สร้าง Maven Project

สร้างโครงสร้างโปรเจกต์

selenium-data-driven/
├── pom.xml
└── src
    └── test
        ├── java
        │   └── com
        │       └── example
        │           └── DataDrivenTest.java
        └── resources
            └── test-data.csv

สร้างโฟลเดอร์บน macOS / Linux:

mkdir -p selenium-data-driven/src/test/java/com/example
mkdir -p selenium-data-driven/src/test/resources
cd selenium-data-driven

Windows PowerShell:

mkdir selenium-data-driven
cd selenium-data-driven
mkdir src\test\java\com\example
mkdir src\test\resources

#5. กำหนด pom.xml

สร้างไฟล์ pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
             http://maven.apache.org/POM/4.0.0
             https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>selenium-data-driven</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <selenium.version>4.49.0</selenium.version>
        <junit.version>6.1.3</junit.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium.version}</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.6.0</version>
            </plugin>

        </plugins>
    </build>

</project>

ดาวน์โหลด Dependency

mvn test-compile

#6. เว็บไซต์สำหรับทดลอง

ตัวอย่างนี้ใช้ Selenium Web Form

https://www.selenium.dev/selenium/web/web-form.html

Element สำคัญ ได้แก่

<input name="my-text">
<button>Submit</button>
<div id="message">Received!</div>

#7. ทดลอง Selenium Test แบบธรรมดาก่อน

สร้างไฟล์

src/test/java/com/example/DataDrivenTest.java

ตัวอย่างพื้นฐาน:

package com.example;

import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DataDrivenTest {

    @Test
    void normalTest() {

        WebDriver driver = new ChromeDriver();

        try {
            driver.get(
                "https://www.selenium.dev/selenium/web/web-form.html"
            );

            driver.findElement(By.name("my-text"))
                    .sendKeys("Selenium");

            driver.findElement(By.cssSelector("button"))
                    .click();

            String message =
                    driver.findElement(By.id("message"))
                            .getText();

            assertEquals("Received!", message);

        } finally {
            driver.quit();
        }
    }
}

รัน

mvn test

#8. Data-Driven Testing ด้วย @ValueSource

กรณี Test Method รับ Parameter เพียงค่าเดียว สามารถใช้ @ValueSource

package com.example;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DataDrivenTest {

    @ParameterizedTest
    @ValueSource(strings = {
        "Selenium",
        "Java",
        "JUnit",
        "Data Driven Testing"
    })
    void testWithValueSource(String inputText) {

        WebDriver driver = new ChromeDriver();

        try {
            driver.get(
                "https://www.selenium.dev/selenium/web/web-form.html"
            );

            driver.findElement(By.name("my-text"))
                    .sendKeys(inputText);

            driver.findElement(By.cssSelector("button"))
                    .click();

            String actual =
                    driver.findElement(By.id("message"))
                            .getText();

            assertEquals("Received!", actual);

        } finally {
            driver.quit();
        }
    }
}

JUnit จะเรียก Test Method เดิมทั้งหมด 4 ครั้ง

Selenium
Java
JUnit
Data Driven Testing

#9. Data-Driven Testing ด้วย @CsvSource

ถ้าต้องการส่ง Parameter หลายค่า สามารถใช้ @CsvSource

package com.example;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DataDrivenTest {

    @ParameterizedTest(name =
        "[{index}] input={0}, expected={1}"
    )
    @CsvSource({
        "Selenium, Received!",
        "'Data Driven Testing', Received!",
        "NPRU, Received!",
        "0123456789, Received!"
    })
    void testWithCsvSource(
            String inputText,
            String expectedMessage) {

        WebDriver driver = new ChromeDriver();

        try {
            driver.get(
                "https://www.selenium.dev/selenium/web/web-form.html"
            );

            driver.findElement(By.name("my-text"))
                    .sendKeys(inputText);

            driver.findElement(By.cssSelector("button"))
                    .click();

            String actualMessage =
                    driver.findElement(By.id("message"))
                            .getText();

            assertEquals(
                    expectedMessage,
                    actualMessage
            );

        } finally {
            driver.quit();
        }
    }
}

ข้อดีคือข้อมูลแต่ละ Row กลายเป็น Test Invocation แยกกัน


#10. Data-Driven Testing จากไฟล์ CSV

สำหรับโครงการจริง การแยกข้อมูลออกเป็นไฟล์ภายนอกจะดูแลรักษาง่ายกว่า

สร้างไฟล์

src/test/resources/test-data.csv

เนื้อหา:

inputText,expectedMessage
Selenium,Received!
Data Driven Testing,Received!
NPRU,Received!
0123456789,Received!
Software Testing,Received!

#11. อ่าน CSV ด้วย @CsvFileSource

package com.example;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DataDrivenTest {

    @ParameterizedTest(
        name = "[{index}] {0} -> {1}"
    )
    @CsvFileSource(
        resources = "/test-data.csv",
        numLinesToSkip = 1
    )
    void testFormWithCsvFile(
            String inputText,
            String expectedMessage) {

        WebDriver driver = new ChromeDriver();

        try {

            driver.get(
                "https://www.selenium.dev/selenium/web/web-form.html"
            );

            driver.findElement(By.name("my-text"))
                    .sendKeys(inputText);

            driver.findElement(By.cssSelector("button"))
                    .click();

            String actualMessage =
                    driver.findElement(By.id("message"))
                            .getText();

            assertEquals(
                    expectedMessage,
                    actualMessage
            );

        } finally {
            driver.quit();
        }
    }
}

JUnit จะอ่าน CSV ทีละแถวและสร้าง Test Invocation ตามจำนวนข้อมูล


#12. ลด Code Duplication ด้วย @BeforeEach และ @AfterEach

ตัวอย่างที่เหมาะกับงานจริงมากขึ้น:

package com.example;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DataDrivenTest {

    private WebDriver driver;

    @BeforeEach
    void setUp() {

        driver = new ChromeDriver();

        driver.manage()
                .window()
                .maximize();
    }

    @AfterEach
    void tearDown() {

        if (driver != null) {
            driver.quit();
        }
    }

    @ParameterizedTest(
        name = "[{index}] input={0}"
    )
    @CsvFileSource(
        resources = "/test-data.csv",
        numLinesToSkip = 1
    )
    void testForm(
            String inputText,
            String expectedMessage) {

        driver.get(
            "https://www.selenium.dev/selenium/web/web-form.html"
        );

        driver.findElement(By.name("my-text"))
                .sendKeys(inputText);

        driver.findElement(By.cssSelector("button"))
                .click();

        String actualMessage =
                driver.findElement(By.id("message"))
                        .getText();

        assertEquals(
                expectedMessage,
                actualMessage
        );
    }
}

ผลคือ Test Method เหลือเฉพาะ Business/Test Logic ที่สำคัญ


#13. เพิ่ม Explicit Wait

Web Application จริงมักมี AJAX, Animation หรือ JavaScript ที่ทำให้ Element ยังไม่พร้อมทันที

สามารถใช้ WebDriverWait

import java.time.Duration;

import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

สร้าง Wait

WebDriverWait wait =
        new WebDriverWait(
                driver,
                Duration.ofSeconds(10)
        );

รอข้อความหลัง Submit

String actualMessage =
        wait.until(
            ExpectedConditions
                .visibilityOfElementLocated(
                    By.id("message")
                )
        ).getText();

ตัวอย่าง:

@ParameterizedTest
@CsvFileSource(
    resources = "/test-data.csv",
    numLinesToSkip = 1
)
void testForm(
        String inputText,
        String expectedMessage) {

    WebDriverWait wait =
            new WebDriverWait(
                    driver,
                    Duration.ofSeconds(10)
            );

    driver.get(
        "https://www.selenium.dev/selenium/web/web-form.html"
    );

    wait.until(
        ExpectedConditions
            .visibilityOfElementLocated(
                By.name("my-text")
            )
    ).sendKeys(inputText);

    driver.findElement(
        By.cssSelector("button")
    ).click();

    String actualMessage =
        wait.until(
            ExpectedConditions
                .visibilityOfElementLocated(
                    By.id("message")
                )
        ).getText();

    assertEquals(
        expectedMessage,
        actualMessage
    );
}

#14. แยก Page Object

เมื่อ Test Suite ใหญ่ขึ้น ไม่ควรใส่ Locator และ WebDriver Steps ทั้งหมดไว้ใน Test Class

สร้าง

src/test/java/com/example/pages/WebFormPage.java
package com.example.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class WebFormPage {

    private final WebDriver driver;

    private final By textInput =
            By.name("my-text");

    private final By submitButton =
            By.cssSelector("button");

    private final By resultMessage =
            By.id("message");

    public WebFormPage(WebDriver driver) {
        this.driver = driver;
    }

    public void open() {
        driver.get(
            "https://www.selenium.dev/selenium/web/web-form.html"
        );
    }

    public void enterText(String text) {
        driver.findElement(textInput)
                .sendKeys(text);
    }

    public void submit() {
        driver.findElement(submitButton)
                .click();
    }

    public String getMessage() {
        return driver.findElement(resultMessage)
                .getText();
    }
}

#15. Test Class หลังใช้ Page Object

package com.example;

import com.example.pages.WebFormPage;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import static org.junit.jupiter.api.Assertions.assertEquals;

class WebFormDataDrivenTest {

    private WebDriver driver;
    private WebFormPage page;

    @BeforeEach
    void setUp() {

        driver = new ChromeDriver();

        page = new WebFormPage(driver);
    }

    @AfterEach
    void tearDown() {

        if (driver != null) {
            driver.quit();
        }
    }

    @ParameterizedTest(
        name = "[{index}] {0}"
    )
    @CsvFileSource(
        resources = "/test-data.csv",
        numLinesToSkip = 1
    )
    void testWebForm(
            String input,
            String expected) {

        page.open();

        page.enterText(input);

        page.submit();

        assertEquals(
            expected,
            page.getMessage()
        );
    }
}

โครงสร้างจะชัดเจนขึ้น

Test Data
     │
     ▼
Parameterized Test
     │
     ▼
Page Object
     │
     ▼
Selenium WebDriver
     │
     ▼
Browser

#16. ตัวอย่าง Data Set สำหรับ Boundary Testing

ไฟล์

boundary-data.csv
inputText,expectedMessage
A,Received!
AB,Received!
1234567890,Received!
ข้อความภาษาไทย,Received!
special_!@#$%,Received!

Test:

@ParameterizedTest
@CsvFileSource(
    resources = "/boundary-data.csv",
    numLinesToSkip = 1
)
void boundaryTest(
        String input,
        String expected) {

    page.open();

    page.enterText(input);

    page.submit();

    assertEquals(
        expected,
        page.getMessage()
    );
}

#17. รองรับข้อมูลหลายชนิด

JUnit Parameterized Test รองรับ Argument Source หลายแบบ เช่น

@ValueSource
@NullSource
@EmptySource
@NullAndEmptySource
@EnumSource
@MethodSource
@CsvSource
@CsvFileSource
@ArgumentsSource

ตัวอย่าง @NullAndEmptySource

@ParameterizedTest
@NullAndEmptySource
void testEmptyInput(String input) {

    // validation test
}

ตัวอย่าง @MethodSource

static Stream<Arguments> testData() {

    return Stream.of(
        Arguments.of(
            "Selenium",
            "Received!"
        ),
        Arguments.of(
            "Java",
            "Received!"
        )
    );
}

ใช้งาน

@ParameterizedTest
@MethodSource("testData")
void testForm(
        String input,
        String expected) {

}

@MethodSource เหมาะเมื่อข้อมูลต้องสร้างแบบ Dynamic หรือเป็น Object ที่ซับซ้อน


#18. Data Source อื่นที่ใช้ร่วมกับ Selenium ได้

ในระบบจริง Test Data อาจมาจากหลายแหล่ง

CSV
 │
Excel
 │
JSON
 │
Database
 │
REST API
 │
Generated Data
 │
 ▼
Test Data Provider
 │
 ▼
Parameterized Test
 │
 ▼
Selenium WebDriver

ตัวอย่างเครื่องมือเสริม:

Data Source Java Library
CSV JUnit @CsvFileSource, Apache Commons CSV
Excel Apache POI
JSON Jackson / Gson
Database JDBC
REST API Java HttpClient / REST Assured
Generated Data Java Faker

#19. รัน Test

รันทั้งหมด

mvn test

รัน Test Class

mvn -Dtest=DataDrivenTest test

รัน Test Class ที่ใช้ Page Object

mvn -Dtest=WebFormDataDrivenTest test

#20. ตัวอย่างผลการรัน

ผลโดยประมาณ:

[INFO] Running com.example.WebFormDataDrivenTest

[1] Selenium
[2] Data Driven Testing
[3] NPRU
[4] 0123456789
[5] Software Testing

Tests run: 5
Failures: 0
Errors: 0
Skipped: 0

BUILD SUCCESS

#21. แนวทางออกแบบ Data-Driven Test ที่ดี

#21.1 อย่าใส่ Test Logic ใน CSV

ไม่ควร:

username,password,clickButton,waitSecond,locator
...

CSV ควรเก็บเฉพาะ Test Data และ Expected Result

ตัวอย่างที่เหมาะกว่า:

username,password,expectedResult
user01,password123,SUCCESS
user02,wrong,INVALID_PASSWORD
unknown,password123,USER_NOT_FOUND

Test Logic ควรอยู่ใน Java


#21.2 ทุก Row ควรมีความหมาย

แต่ละแถวควรแทน Test Scenario ที่ต้องการจริง

เช่น:

Valid
Invalid
Boundary
Empty
Special Character
Unicode
Long Input

ไม่ควรเพิ่มข้อมูลจำนวนมากโดยไม่มีเป้าหมาย


#21.3 Test ควร Independent

แต่ละ Dataset ไม่ควรพึ่งผลของ Dataset ก่อนหน้า

ไม่ควร:

Row 1 = Create User
Row 2 = Login User
Row 3 = Delete User

เพราะหาก Row 1 ล้มเหลว Row อื่นอาจล้มเหลวตาม


#21.4 เปิด Browser ใหม่ต่อ Test Invocation

รูปแบบ

@BeforeEach
void setUp() {
    driver = new ChromeDriver();
}

ช่วยลด State Leakage เช่น

Cookie
Session
Local Storage
Previous Page State

#22. Data-Driven Testing กับ CI/CD

เมื่อ Test พร้อมแล้วสามารถรันใน Pipeline

ตัวอย่าง GitHub Actions:

name: Selenium Tests

on:
  push:
  pull_request:

jobs:

  test:

    runs-on: ubuntu-latest

    steps:

      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'
          cache: maven

      - name: Run Selenium Tests
        run: mvn test

สำหรับ CI ควรใช้ Chrome Headless

ตัวอย่าง:

ChromeOptions options =
        new ChromeOptions();

options.addArguments("--headless=new");

driver = new ChromeDriver(options);

#23. Architecture ที่แนะนำ

สำหรับโครงการขนาดกลางถึงใหญ่:

src/test/java
│
├── tests
│   ├── LoginTest.java
│   ├── RegisterTest.java
│   └── CheckoutTest.java
│
├── pages
│   ├── LoginPage.java
│   ├── RegisterPage.java
│   └── CheckoutPage.java
│
├── utils
│   ├── DriverFactory.java
│   └── TestDataLoader.java
│
└── models
    └── TestData.java


src/test/resources
│
├── login-data.csv
├── register-data.csv
└── checkout-data.csv

Flow:

Test Data
    │
    ▼
Parameterized Test
    │
    ▼
Page Object
    │
    ▼
Driver Factory
    │
    ▼
Selenium WebDriver
    │
    ▼
Chrome / Firefox / Edge

#24. @CsvSource กับ @CsvFileSource ต่างกันอย่างไร

หัวข้อ @CsvSource @CsvFileSource
ที่เก็บข้อมูล ใน Java code CSV file
เหมาะกับ Dataset เล็ก Dataset กลาง/ใหญ่
แก้ข้อมูลโดยไม่แก้ Java ไม่ได้ ได้
อ่านง่ายเมื่อข้อมูลเยอะ น้อยกว่า ดีกว่า
ใช้ใน CI/CD ได้ ได้
Maintenance ง่ายเมื่อข้อมูลน้อย เหมาะกับงานจริงกว่า

คำแนะนำ:

1–5 rows
    → @CsvSource

หลาย rows / เปลี่ยนข้อมูลบ่อย
    → @CsvFileSource

ข้อมูลซับซ้อน / Dynamic
    → @MethodSource หรือ Custom Provider

#25. สรุป

Data-Driven Testing ช่วยเปลี่ยนรูปแบบจาก

1 Test
+
1 Data Set

ให้เป็น

1 Test Logic
+
Many Data Sets

แนวทางที่แนะนำสำหรับ Selenium Java คือ

CSV
  ↓
JUnit Parameterized Test
  ↓
Page Object
  ↓
Selenium WebDriver
  ↓
Browser
  ↓
Assertion

เริ่มต้นง่ายที่สุดด้วย

@ParameterizedTest
@CsvSource(...)

และเมื่อโครงการใหญ่ขึ้นให้แยกข้อมูลออกเป็น

@CsvFileSource(...)

จากนั้นสามารถขยายไปสู่

Excel
JSON
Database
REST API
Dynamic Test Data

ได้โดยไม่จำเป็นต้องเปลี่ยน WebDriver Test Logic หลักมากนัก


#References