テストツール

Selenium

概要

SeleniumはWebアプリケーション自動化のデファクトスタンダードとして確立されたブラウザ自動化フレームワークです。多言語・多ブラウザ対応、長い実績と豊富なエコシステム、WebDriverプロトコルの基盤として業界標準の地位を築いています。Java、Python、C#、JavaScript、Ruby等の主要プログラミング言語をサポートし、Chrome、Firefox、Safari、Edgeといった全てのモダンブラウザでの自動化が可能。Selenium Grid機能による分散テスト実行、Page Object Modelによる保守性の高いテスト設計、豊富なサードパーティツールとの連携により、エンタープライズレベルのWebアプリケーションテストインフラストラクチャを構築できます。

詳細

Selenium 2025年版は、20年以上の開発実績により培われた安定性と信頼性を誇る成熟したWebブラウザ自動化フレームワークです。WebDriver W3C標準仕様の策定を主導し、現在のブラウザ自動化の基盤技術となっています。Selenium WebDriverは直接ブラウザとネイティブ通信を行い、JavaScriptを介さない高精度な要素操作を実現。Selenium IDE(ブラウザ拡張機能)によるコードなしテスト記録・再生、Selenium Grid機能による大規模分散テスト実行、docker-seleniumコンテナによるクラウドネイティブ環境での運用など、あらゆる規模のテスト要件に対応します。Maven/Gradle、pip、npm、NuGet等の各言語パッケージマネージャーでの容易な導入、JUnit/TestNG、pytest、NUnit、Mocha等のテストフレームワークとの完璧な統合、Jenkins、GitHub Actions、Azure DevOps等のCI/CDツールとの親和性により、既存開発環境への導入障壁を最小化しています。

主な特徴

  • 業界標準のWebDriver プロトコル: W3C標準仕様に基づく安定したブラウザ自動化
  • 真の多言語サポート: Java、Python、C#、JavaScript、Ruby、Go等の主要言語対応
  • 包括的ブラウザサポート: Chrome、Firefox、Safari、Edge、Opera等全ブラウザ対応
  • Selenium Grid: 分散並列テスト実行とクロスブラウザテストのスケーリング
  • 成熟したエコシステム: 豊富なサードパーティツール、プラグイン、サポートコミュニティ
  • エンタープライズ対応: 大規模組織での実績とセキュリティ・コンプライアンス対応

メリット・デメリット

メリット

  • 20年以上の実績による高い安定性と信頼性、業界標準としての地位
  • 多言語サポートによる既存開発チームのスキルセット活用と学習コスト削減
  • 全主要ブラウザ対応による真のクロスブラウザテストと広範囲な検証
  • 豊富なドキュメント、チュートリアル、コミュニティサポートと学習リソース
  • サードパーティツール、IDE統合、CI/CD連携等の充実したエコシステム
  • Selenium Gridによる大規模分散テスト実行とリソース効率化
  • オープンソースによる無償利用と柔軟なカスタマイズ性
  • エンタープライズ環境での豊富な実装事例とベストプラクティス

デメリット

  • 現代的なテストツール(Playwright、Cypress)と比較してセットアップ複雑性
  • 明示的な待機処理が必要でフラキーテストが発生しやすい傾向
  • ブラウザドライバーの個別管理とバージョン同期の煩雑さ
  • 比較的低速なテスト実行速度と大量テストでのリソース消費
  • 古いアーキテクチャによる一部の現代Webアプリケーション対応課題
  • デバッグ機能とテスト記録・再生機能の現代ツールとの機能差

参考ページ

書き方の例

インストールとセットアップ

# Java (Maven)
# pom.xmlに追加
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.27.0</version>
</dependency>

# Python
pip install selenium
pip install webdriver-manager  # ドライバー自動管理

# JavaScript/Node.js
npm install selenium-webdriver

# C# (.NET)
dotnet add package Selenium.WebDriver
dotnet add package Selenium.WebDriver.ChromeDriver

# Ruby
gem install selenium-webdriver

# WebDriverManager (ドライバー自動管理)
pip install webdriver-manager  # Python
npm install chromedriver  # Node.js

基本的なブラウザ操作(Python)

# test_basic_operations.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import unittest

class BasicWebDriverTest(unittest.TestCase):
    
    def setUp(self):
        # ChromeDriverの自動管理とセットアップ
        service = Service(ChromeDriverManager().install())
        options = webdriver.ChromeOptions()
        options.add_argument('--headless')  # ヘッドレスモード
        options.add_argument('--no-sandbox')
        options.add_argument('--disable-dev-shm-usage')
        
        self.driver = webdriver.Chrome(service=service, options=options)
        self.driver.implicitly_wait(10)  # 暗黙的待機
        self.wait = WebDriverWait(self.driver, 10)  # 明示的待機
    
    def tearDown(self):
        self.driver.quit()
    
    def test_basic_navigation(self):
        """基本的なページナビゲーションテスト"""
        self.driver.get('https://example.com')
        
        # ページタイトルの確認
        self.assertIn('Example Domain', self.driver.title)
        
        # 要素の存在確認
        heading = self.driver.find_element(By.TAG_NAME, 'h1')
        self.assertEqual('Example Domain', heading.text)
        
        # URLの確認
        self.assertEqual('https://example.com/', self.driver.current_url)
    
    def test_form_interaction(self):
        """フォーム操作のテスト"""
        self.driver.get('https://example.com/contact')
        
        # フォーム要素の操作
        name_field = self.wait.until(
            EC.presence_of_element_located((By.NAME, 'name'))
        )
        name_field.send_keys('田中太郎')
        
        email_field = self.driver.find_element(By.NAME, 'email')
        email_field.send_keys('[email protected]')
        
        # テキストエリアの操作
        message_field = self.driver.find_element(By.NAME, 'message')
        message_field.clear()
        message_field.send_keys('お問い合わせ内容です。')
        
        # チェックボックスとラジオボタン
        checkbox = self.driver.find_element(By.ID, 'agree-terms')
        if not checkbox.is_selected():
            checkbox.click()
        
        # ドロップダウン選択
        from selenium.webdriver.support.ui import Select
        dropdown = Select(self.driver.find_element(By.NAME, 'category'))
        dropdown.select_by_visible_text('技術サポート')
        
        # 送信ボタンクリック
        submit_button = self.driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]')
        submit_button.click()
        
        # 成功メッセージの確認
        success_message = self.wait.until(
            EC.presence_of_element_located((By.CLASS_NAME, 'success-message'))
        )
        self.assertIn('送信完了', success_message.text)

if __name__ == '__main__':
    unittest.main()

Java版の例

// BasicSeleniumTest.java
import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;

public class BasicSeleniumTest {
    
    private WebDriver driver;
    private WebDriverWait wait;
    
    @BeforeAll
    static void setupClass() {
        WebDriverManager.chromedriver().setup();
    }
    
    @BeforeEach
    void setUp() {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless");
        options.addArguments("--no-sandbox");
        options.addArguments("--disable-dev-shm-usage");
        
        driver = new ChromeDriver(options);
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }
    
    @AfterEach
    void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
    
    @Test
    void testBasicNavigation() {
        driver.get("https://example.com");
        
        // ページタイトルの確認
        assertTrue(driver.getTitle().contains("Example Domain"));
        
        // 要素の存在確認
        WebElement heading = driver.findElement(By.tagName("h1"));
        assertEquals("Example Domain", heading.getText());
        
        // URLの確認
        assertEquals("https://example.com/", driver.getCurrentUrl());
    }
    
    @Test
    void testFormInteraction() {
        driver.get("https://example.com/contact");
        
        // フォーム要素の操作
        WebElement nameField = wait.until(
            ExpectedConditions.presenceOfElementLocated(By.name("name"))
        );
        nameField.sendKeys("田中太郎");
        
        WebElement emailField = driver.findElement(By.name("email"));
        emailField.sendKeys("[email protected]");
        
        // テキストエリアの操作
        WebElement messageField = driver.findElement(By.name("message"));
        messageField.clear();
        messageField.sendKeys("お問い合わせ内容です。");
        
        // ドロップダウン選択
        Select dropdown = new Select(driver.findElement(By.name("category")));
        dropdown.selectByVisibleText("技術サポート");
        
        // チェックボックス操作
        WebElement checkbox = driver.findElement(By.id("agree-terms"));
        if (!checkbox.isSelected()) {
            checkbox.click();
        }
        
        // 送信ボタンクリック
        WebElement submitButton = driver.findElement(
            By.cssSelector("button[type='submit']")
        );
        submitButton.click();
        
        // 成功メッセージの確認
        WebElement successMessage = wait.until(
            ExpectedConditions.presenceOfElementLocated(By.className("success-message"))
        );
        assertTrue(successMessage.getText().contains("送信完了"));
    }
    
    @Test
    void testMultipleWindows() {
        driver.get("https://example.com");
        
        String originalWindow = driver.getWindowHandle();
        
        // 新しいタブを開くリンクをクリック
        driver.findElement(By.linkText("新しいタブで開く")).click();
        
        // 新しいウィンドウが開くまで待機
        wait.until(ExpectedConditions.numberOfWindowsToBe(2));
        
        // 新しいウィンドウに切り替え
        for (String windowHandle : driver.getWindowHandles()) {
            if (!originalWindow.equals(windowHandle)) {
                driver.switchTo().window(windowHandle);
                break;
            }
        }
        
        // 新しいウィンドウでの操作
        assertTrue(driver.getTitle().contains("新しいページ"));
        
        // 元のウィンドウに戻る
        driver.switchTo().window(originalWindow);
        assertTrue(driver.getTitle().contains("Example Domain"));
    }
}

JavaScript版の例

// test_selenium_js.js
const { Builder, By, Key, until } = require('selenium-webdriver');
const { expect } = require('chai');

describe('Selenium WebDriver Tests', function() {
    let driver;
    
    before(async function() {
        // ドライバーのセットアップ
        driver = await new Builder()
            .forBrowser('chrome')
            .setChromeOptions(new (require('selenium-webdriver/chrome')).Options()
                .addArguments('--headless')
                .addArguments('--no-sandbox')
                .addArguments('--disable-dev-shm-usage')
            )
            .build();
    });
    
    after(async function() {
        await driver.quit();
    });
    
    it('基本的なページナビゲーション', async function() {
        await driver.get('https://example.com');
        
        // ページタイトルの確認
        const title = await driver.getTitle();
        expect(title).to.contain('Example Domain');
        
        // 要素の存在確認
        const heading = await driver.findElement(By.tagName('h1'));
        const headingText = await heading.getText();
        expect(headingText).to.equal('Example Domain');
    });
    
    it('フォーム操作とインタラクション', async function() {
        await driver.get('https://example.com/contact');
        
        // フォーム入力
        const nameField = await driver.wait(
            until.elementLocated(By.name('name')), 
            10000
        );
        await nameField.sendKeys('田中太郎');
        
        const emailField = await driver.findElement(By.name('email'));
        await emailField.sendKeys('[email protected]');
        
        // テキストエリア操作
        const messageField = await driver.findElement(By.name('message'));
        await messageField.clear();
        await messageField.sendKeys('お問い合わせ内容です。');
        
        // Enter キーでフォーム送信
        await messageField.sendKeys(Key.RETURN);
        
        // 成功メッセージの確認
        const successMessage = await driver.wait(
            until.elementLocated(By.className('success-message')),
            10000
        );
        const messageText = await successMessage.getText();
        expect(messageText).to.contain('送信完了');
    });
    
    it('待機条件とタイムアウト処理', async function() {
        await driver.get('https://example.com/dynamic');
        
        // 要素が表示されるまで待機
        await driver.wait(until.elementIsVisible(
            driver.findElement(By.id('dynamic-content'))
        ), 15000);
        
        // クリック可能になるまで待機
        const button = await driver.findElement(By.id('async-button'));
        await driver.wait(until.elementIsEnabled(button), 10000);
        await button.click();
        
        // テキストが変更されるまで待機
        await driver.wait(until.elementTextContains(
            driver.findElement(By.id('result')), 
            '処理完了'
        ), 20000);
    });
});

Page Object Modelパターン

# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
    
    def find_element(self, locator):
        return self.wait.until(EC.presence_of_element_located(locator))
    
    def click_element(self, locator):
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.click()
    
    def enter_text(self, locator, text):
        element = self.find_element(locator)
        element.clear()
        element.send_keys(text)

# pages/login_page.py
from selenium.webdriver.common.by import By
from .base_page import BasePage

class LoginPage(BasePage):
    # ロケーター定義
    USERNAME_FIELD = (By.NAME, 'username')
    PASSWORD_FIELD = (By.NAME, 'password')
    LOGIN_BUTTON = (By.CSS_SELECTOR, 'button[type="submit"]')
    ERROR_MESSAGE = (By.CLASS_NAME, 'error-message')
    
    def __init__(self, driver):
        super().__init__(driver)
        self.url = 'https://example.com/login'
    
    def load(self):
        self.driver.get(self.url)
        return self
    
    def login(self, username, password):
        self.enter_text(self.USERNAME_FIELD, username)
        self.enter_text(self.PASSWORD_FIELD, password)
        self.click_element(self.LOGIN_BUTTON)
        return self
    
    def get_error_message(self):
        error_element = self.find_element(self.ERROR_MESSAGE)
        return error_element.text
    
    def is_login_successful(self):
        return '/dashboard' in self.driver.current_url

# pages/dashboard_page.py
from selenium.webdriver.common.by import By
from .base_page import BasePage

class DashboardPage(BasePage):
    WELCOME_MESSAGE = (By.CLASS_NAME, 'welcome-message')
    LOGOUT_LINK = (By.LINK_TEXT, 'ログアウト')
    
    def get_welcome_message(self):
        element = self.find_element(self.WELCOME_MESSAGE)
        return element.text
    
    def logout(self):
        self.click_element(self.LOGOUT_LINK)

# tests/test_login.py
import unittest
from selenium import webdriver
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage

class LoginTest(unittest.TestCase):
    
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.login_page = LoginPage(self.driver)
        self.dashboard_page = DashboardPage(self.driver)
    
    def tearDown(self):
        self.driver.quit()
    
    def test_successful_login(self):
        """正常ログインのテスト"""
        self.login_page.load()
        self.login_page.login('validuser', 'validpass')
        
        # ダッシュボードページへの遷移確認
        self.assertTrue(self.login_page.is_login_successful())
        
        # ウェルカムメッセージの確認
        welcome_message = self.dashboard_page.get_welcome_message()
        self.assertIn('ようこそ', welcome_message)
    
    def test_invalid_login(self):
        """無効な認証情報でのログインテスト"""
        self.login_page.load()
        self.login_page.login('invaliduser', 'invalidpass')
        
        # エラーメッセージの確認
        error_message = self.login_page.get_error_message()
        self.assertIn('認証に失敗しました', error_message)
        
        # ログインページに留まることを確認
        self.assertFalse(self.login_page.is_login_successful())

Selenium Grid設定

# docker-compose.yml - Selenium Grid設定
version: '3.8'
services:
  selenium-hub:
    image: selenium/hub:4.15.0
    container_name: selenium-hub
    ports:
      - "4444:4444"
      - "4442:4442"
      - "4443:4443"
    environment:
      - GRID_MAX_SESSION=16
      - GRID_BROWSER_TIMEOUT=300
      - GRID_TIMEOUT=300

  chrome-node:
    image: selenium/node-chrome:4.15.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444
      - NODE_MAX_INSTANCES=2
      - NODE_MAX_SESSION=2
    scale: 2

  firefox-node:
    image: selenium/node-firefox:4.15.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444
      - NODE_MAX_INSTANCES=2
      - NODE_MAX_SESSION=2
    scale: 2

  edge-node:
    image: selenium/node-edge:4.15.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444
      - NODE_MAX_INSTANCES=1
      - NODE_MAX_SESSION=1
# test_grid.py - Grid対応テスト
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import unittest

class GridTest(unittest.TestCase):
    
    def setUp(self):
        # Grid Hub接続設定
        self.hub_url = 'http://localhost:4444/wd/hub'
    
    def test_chrome_grid(self):
        """Chrome on Gridでのテスト"""
        capabilities = DesiredCapabilities.CHROME
        capabilities['browserVersion'] = 'latest'
        capabilities['platformName'] = 'linux'
        
        driver = webdriver.Remote(
            command_executor=self.hub_url,
            desired_capabilities=capabilities
        )
        
        try:
            driver.get('https://example.com')
            self.assertIn('Example Domain', driver.title)
        finally:
            driver.quit()
    
    def test_firefox_grid(self):
        """Firefox on Gridでのテスト"""
        capabilities = DesiredCapabilities.FIREFOX
        capabilities['browserVersion'] = 'latest'
        capabilities['platformName'] = 'linux'
        
        driver = webdriver.Remote(
            command_executor=self.hub_url,
            desired_capabilities=capabilities
        )
        
        try:
            driver.get('https://example.com')
            self.assertIn('Example Domain', driver.title)
        finally:
            driver.quit()
    
    def test_parallel_execution(self):
        """並列実行テスト"""
        import concurrent.futures
        
        def run_test(browser):
            if browser == 'chrome':
                capabilities = DesiredCapabilities.CHROME
            else:
                capabilities = DesiredCapabilities.FIREFOX
            
            driver = webdriver.Remote(
                command_executor=self.hub_url,
                desired_capabilities=capabilities
            )
            
            try:
                driver.get('https://example.com')
                return driver.title
            finally:
                driver.quit()
        
        browsers = ['chrome', 'firefox', 'chrome', 'firefox']
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
            futures = [executor.submit(run_test, browser) for browser in browsers]
            results = [future.result() for future in futures]
        
        for result in results:
            self.assertIn('Example Domain', result)

CI/CD統合(GitHub Actions)

# .github/workflows/selenium-tests.yml
name: Selenium Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [chrome, firefox]
        python-version: [3.8, 3.9, '3.10']
    
    services:
      selenium:
        image: selenium/standalone-chrome:4.15.0
        ports:
          - 4444:4444
        options: --shm-size=2g
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install selenium pytest webdriver-manager
        pip install -r requirements.txt
    
    - name: Install browser binaries
      run: |
        sudo apt-get update
        sudo apt-get install -y wget gnupg
        # Chrome
        wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
        sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
        sudo apt-get update
        sudo apt-get install -y google-chrome-stable
        # Firefox
        sudo apt-get install -y firefox
    
    - name: Run Selenium tests
      run: |
        pytest tests/ -v --browser=${{ matrix.browser }}
      env:
        SELENIUM_GRID_URL: http://localhost:4444/wd/hub
    
    - name: Upload test results
      uses: actions/upload-artifact@v4
      if: failure()
      with:
        name: test-results-${{ matrix.browser }}-${{ matrix.python-version }}
        path: |
          screenshots/
          test-reports/

デバッグとトラブルシューティング

# debug_helpers.py
import os
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class DebugHelper:
    
    def __init__(self, driver):
        self.driver = driver
        self.screenshot_dir = 'screenshots'
        os.makedirs(self.screenshot_dir, exist_ok=True)
    
    def take_screenshot(self, name):
        """スクリーンショット撮影"""
        timestamp = int(time.time())
        filename = f"{name}_{timestamp}.png"
        filepath = os.path.join(self.screenshot_dir, filename)
        self.driver.save_screenshot(filepath)
        print(f"Screenshot saved: {filepath}")
        return filepath
    
    def get_page_source(self, name):
        """ページソース保存"""
        timestamp = int(time.time())
        filename = f"{name}_{timestamp}.html"
        filepath = os.path.join(self.screenshot_dir, filename)
        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(self.driver.page_source)
        print(f"Page source saved: {filepath}")
        return filepath
    
    def debug_element(self, locator):
        """要素のデバッグ情報出力"""
        try:
            element = self.driver.find_element(*locator)
            print(f"Element found: {locator}")
            print(f"  Tag: {element.tag_name}")
            print(f"  Text: {element.text}")
            print(f"  Displayed: {element.is_displayed()}")
            print(f"  Enabled: {element.is_enabled()}")
            print(f"  Location: {element.location}")
            print(f"  Size: {element.size}")
            return element
        except Exception as e:
            print(f"Element not found: {locator}")
            print(f"  Error: {e}")
            self.take_screenshot(f"error_{locator[1]}")
            return None
    
    def wait_and_debug(self, locator, timeout=10):
        """待機とデバッグの組み合わせ"""
        wait = WebDriverWait(self.driver, timeout)
        try:
            element = wait.until(EC.presence_of_element_located(locator))
            print(f"Element appeared after waiting: {locator}")
            return element
        except Exception as e:
            print(f"Element did not appear within {timeout}s: {locator}")
            print(f"Current URL: {self.driver.current_url}")
            print(f"Page title: {self.driver.title}")
            self.take_screenshot(f"timeout_{locator[1]}")
            self.get_page_source(f"timeout_{locator[1]}")
            raise e

# 使用例
def test_with_debug(self):
    debug = DebugHelper(self.driver)
    
    self.driver.get('https://example.com/complex-page')
    
    # デバッグ情報付きで要素を探す
    button = debug.debug_element((By.ID, 'submit-button'))
    
    if button:
        button.click()
    else:
        # 要素が見つからない場合の代替処理
        debug.take_screenshot('button_not_found')
        self.fail('Submit button not found')
    
    # 待機とデバッグ
    result = debug.wait_and_debug((By.CLASS_NAME, 'result'), timeout=15)
    self.assertIsNotNone(result)