1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポテンシャルを引き出すTDD×アジャイル開発

Last updated at Posted at 2025-02-27

はじめに

最近、アジャイル開発について調べる機会があった。様々なウェブサイトや記事を読み漁っていくうちに、ある違和感が私の中で大きくなっていった。

それは、具体的な手法や実践方法について語られることが意外なほど少ないということだ。代わりに目につくのは、「柔軟な対応」「迅速な開発」といった抽象的なキーワードばかり。まるで、ふわふわした理想論を並べ立てているような印象を受けた。

本来、アジャイル開発は、ソフトウェア開発の現場から生まれた実践的な手法のはずだ。2001年に「アジャイルソフトウェア開発宣言」が発表された際、その創設者たちが目指したのは、より効果的で人間的な開発プロセスだったはずだ。

しかし今、目にする情報の多くは、その本質から遠ざかってしまっているように感じる。 具体性を欠いた美辞麗句の陰に、開発者たちへの過度な要求や、マネジメント側の都合の良い解釈 が見え隠れする。

私たちは、アジャイル開発の本来の意図に立ち返り、より具体的で実践的な議論を交わすべきではないだろうか。それこそが、創設者たちの思いを受け継ぎ、真の意味でのアジャイル開発を実現する道なのかもしれない。

 本来 "寛容さ""インクリメンタルなアップデート" は駆け引きに使用されるものではなく、変化に耐えうる体制づくりのための方針でしかないのです。

テスト駆動開発(TDD) はアジャイルらしい漸進的なロールアウトの実装を可能にする。今回はアジャイル開発例にTDDの手法を積極的に取り入れた。

 アジャイルマインドセット、アジャイルリーダーシップコーチのGil Broza氏もこう言っているように、効果的なアジャイル開発では 心理的安全性 が感じられる状態になっている。

アジャイルの観点では、デイリースクラムはチームメンバーが重要な作業を終わらせる為にお互いに協力し合って自己組織化している場合に一番効果が出る。透明性 を保ち、全てのことに誠実に参加できるような心理的安全性 を感じられるからだ。

そこで、今回こちらの記事では、私が「マッチングアプリ開発」を例に、アジャイル開発で 最低限運用されるべき「仮のバックログ作成例」 を挙げていきたいと思います。
 また、アジャイルにおいてテスト駆動開発をするにあたり一番労力のかかると言われがちな最初の基盤構築にあたる 第一スプリント走り出しの具体例 を紹介していきたいと思います。

アジャイルソフトウェア開発宣言

・プロセスやツールよりも個人と対話を
・包括的なドキュメントよりも動くソフトウェアを
・契約交渉よりも顧客との協調を
・計画に従うことよりも変化への対応を

これはアジャイル開発に携わる方ならば何度も見かけたことがあると思います。
"無神経・無関心であること" に対して、 "目を通し、頭には入れているけれど寛容である" ことは明確に違います。

・プロセスやツールよりも個人と対話を
→個人と対話しやすくなる為のツールは必要です。
・包括的なドキュメントよりも動くソフトウェアを
→動かす為に迅速なデプロイを可能にするCIが必要です
・契約交渉よりも顧客との協調を
→顧客はビジネスの取引相手です。サービスをしながらも、「何をもって協調するか」の基準がなくては、エンジニアはボランティアではありません。
・計画に従うことよりも変化への対応を
→変化に適切に対応するには、何が起こるか仮の予想を立てておく事は大切です。計画せず変化に対応するのではなく、仮の計画をして、そして変化に対応することで、変化に適切に対応できるようになります。

変化に対応する為に、 テスト駆動開発(TDD) が必要になります。
それによって、小さな単位での開発が可能になります。

プロダクトバックログ例の紹介にあたって

 今回は、SpringBOOT×Thymeleafの構成でマッチングアプリを開発した場合のケースです。
 チームメンバーは以下の構成を想定しています。その為、実際にプロジェクトを遂行する際はメンバーの能力や人数によってスプリントに充てられるタスクの量は適宜調整される必要があります。

前提条件
チームサイズ:4-5名程度
一つのスプリントは2週間を想定

バックエンドエンジニア:2-3名
フロントエンドエンジニア:1-2名
QAエンジニア:1名

必要な技術レベル:
バックエンドエンジニア
SpringBoot、Spring Securityの実務経験
TDD/BDDの実践経験
WebSocketやRESTful APIの設計・実装経験
フロントエンドエンジニア
Thymeleafの実務経験
モダンなUIコンポーネント設計の経験
レスポンシブデザインの実装経験
QAエンジニア
Seleniumを含むE2Eテスト自動化の実務経験
テスト設計・テスト戦略立案の経験
CI/CD環境の構築・運用経験

全体として、以下のスキルを持つメンバーが必要です:

アジャイル開発の実践経験
テスト駆動開発の理解と実践経験
Git/GitHubでのチーム開発経験
コードレビューの経験

プロダクトバックログ例

スプリント0(スケルトン構築):

  • Selenium設定と基本構成
    • WebDriverManagerの設定
    • JUnitの設定
    • WebDriverの初期化処理の共通化
    • 明示的待機・暗黙的待機の設定
  • 全体E2Eテストスケルトン作成
    • PageFactoryパターンによる画面クラス定義
    • ユーザーフロー全体のSeleniumテストケース作成
  • CI/CD環境構築(Selenium Grid含む)
  • プロジェクトの基本設定

スプリント1(認証基盤):

  • Seleniumテスト作成
    • ログインページクラス
    • 登録ページクラス
    • WebElement定義
    • 認証フローのE2Eテストケース
  • Spring Security実装
  • ユーザー認証の実装
  • 基本的なUIテンプレート
  • 単体テスト作成

スプリント2(プロフィール管理):

  • Seleniumテスト作成
    • プロフィールページクラス
    • ファイルアップロード処理のテストケース
    • プロフィール編集フローのE2Eテスト
  • プロフィールドメインモデル実装
  • プロフィールCRUD機能
  • 画像アップロード機能
  • 統合テスト作成

スプリント3(マッチング機能):

  • Seleniumテスト作成
    • ユーザー検索ページクラス
    • いいね・マッチング操作のテストケース
    • JavaScriptExecutor活用部分の実装
  • ユーザー検索機能
  • いいね機能
  • マッチングロジック
  • マッチング通知機能

スプリント4(メッセージング):

  • Seleniumテスト作成
    • チャットページクラス
    • 動的要素の待機処理実装
    • WebSocket通信のテストケース
  • WebSocket実装
  • チャットUI実装
  • メッセージングドメインモデル実装
  • リアルタイム通知機能

スプリント5(検索機能拡張):

  • Seleniumテスト作成
    • 詳細検索ページクラス
    • フィルター操作のテストケース
    • 動的コンテンツのテスト実装
  • 検索条件の拡張実装
  • 位置情報検索実装
  • 検索パフォーマンス最適化
  • 負荷テスト実施

スプリント6(補完機能):

  • Seleniumテスト作成
    • 設定ページクラス
    • 管理者ページクラス
    • クロスブラウザテストの実装
  • プライバシー設定実装
  • 管理者機能実装
  • セキュリティ強化
  • 全E2Eテストの総合実行

各スプリントでの開発サイクル:

  1. PageFactoryによるページクラス作成
  2. E2Eテストケース実装
  3. 単体テスト作成
  4. ドメインモデル実装
  5. UI実装
  6. 統合テスト
  7. リファクタリング

具体的には

1.PageFactoryによるページクラス作成

まず、SignupPage.java を作成し、PageFactoryを利用してUI要素を管理します。

SignupPage.java
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class SignupPage {
    private WebDriver driver;

    // PageFactoryを利用して要素を定義
    @FindBy(id = "username")
    private WebElement usernameField;

    @FindBy(id = "email")
    private WebElement emailField;

    @FindBy(id = "password")
    private WebElement passwordField;

    @FindBy(id = "signup-button")
    private WebElement signupButton;

    @FindBy(id = "success-message")
    private WebElement successMessage;

    // コンストラクタでPageFactoryを初期化
    public SignupPage(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    // ユーザー登録処理をメソッド化
    public void registerUser(String username, String email, String password) {
        usernameField.sendKeys(username);
        emailField.sendKeys(email);
        passwordField.sendKeys(password);
        signupButton.click();
    }

    // 登録完了メッセージの取得
    public String getSuccessMessage() {
        return successMessage.getText();
    }
}

2. スケルトンの作成

テストを先に書き、未実装のコードはスケルトンとして用意します。
一番シンプルな最初の正常系を作ることから始めよう。
ここではマッチングアプリのサインアップ(会員登録)の操作をとりあげた。
Spring Boot & Seleniumを使うため、spring-boot-starter-testselenium-java を依存関係に追加します(Gradleの場合)。

build.gradle
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.seleniumhq.selenium:selenium-java:4.12.1'
    testImplementation 'org.seleniumhq.selenium:selenium-chrome-driver:4.12.1'
}

UserSignupEndToEndTest.java
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.PageFactory;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@ExtendWith(SpringExtension.class)
public class UserSignupEndToEndTest {
    
    private WebDriver driver;
    private SignupPage signupPage;
    
    @BeforeEach
    public void setUp() {
        System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver");
        driver = new ChromeDriver();
        driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
        
        // サインアップページを開く
        driver.get("http://localhost:8080/signup");
        // PageFactoryを使用してページオブジェクトを初期化
        signupPage = PageFactory.initElements(driver, SignupPage.class);
    }
    
     // サインアップページで有効なユーザーの情報を送信すると成功メッセージが表示される。
    @Test
    public void shouldShowSuccessMessageWhenSubmittingValidUserDetailsOnSignupPage() {
        // 未実装のフォーム入力
        String username = "testuser";
        String email = "test@example.com";
        String password = "password123";
        
        // フォーム送信
        signupPage.registerUser(username, email, password);
        
        // 期待する結果
        assertThat(signupPage.getSuccessMessage(), is("登録完了しました!"));
    }
    
    @AfterEach
    public void tearDown() {
        driver.quit();
    }
}

この時点では、まだ登録機能の実装がないため、テストを実行すると失敗します。

3. 最小限の実装

次に、このテストを通すためにSpring BootのControllerHTMLの実装を進めていきます。
分担して、テストコードを書いた人とは違うメンバーがここを担当するのも良いでしょう。
可読性の良好なテストコードがあれば、責任の範囲が明確なのでタスクの分担もしやすいですね。

SignupController.java
@Controller
@RequestMapping("/signup")
public class SignupController {

    @GetMapping
    public String showSignupForm() {
        return "signup";
    }

    @PostMapping
    public String registerUser(@RequestParam String username,
                               @RequestParam String email,
                               @RequestParam String password,
                               Model model) {
        // 仮の登録処理(まだDBなし)
        model.addAttribute("message", "登録完了しました!");
        return "signup";
    }
}

signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Signup</title>
</head>
<body>
    <h2>ユーザー登録</h2>
    <form action="/signup" method="post">
        <label for="username">ユーザー名:</label>
        <input type="text" id="username" name="username" required><br>

        <label for="email">メール:</label>
        <input type="email" id="email" name="email" required><br>

        <label for="password">パスワード:</label>
        <input type="password" id="password" name="password" required><br>

        <button type="submit" id="signup-button">登録</button>
    </form>

    <p id="success-message" th:text="${message}"></p>
</body>
</html>

これで、最小限の登録画面が動作し、テストを実行すると成功するようになります。

4. 実装を拡張する

 TDDのサイクルに従い、次のテストケースを追加しながら、ユーザー登録をデータベースに保存する処理を実装します。
 このように実装のステップが明確であると、チームプレーがしやすいのがハッキリ分かるかと思います。ここのタスクにまたメンバーを一人充てても良いのです。

 例えば、パスワードのハッシュ化を追加 し、 UserRepository を作成し、DBに保存できるようにします。

UserService.java
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    public void registerUser(String username, String email, String password) {
        User user = new User(username, email, passwordEncoder.encode(password));
        userRepository.save(user);
    }
}

次のステップ

ログインのE2Eテスト を追加し、認証機能を実装

同様に以下のような機能もE2Eテストを先に記述し、それを満たすようにコーディングしていきます。
プロフィール編集のテスト を追加し、編集画面を作成
マッチング機能のテスト を追加し、「いいね」処理を実装
メッセージ機能のテスト を追加し、WebSocketまたはREST APIでメッセージ送信を実装

最後に

これらの開発フローがルーチン化すれば、基盤構築後は変化に強く心理的安全性の強い開発チームが出来上がるだろう。

最後に辛口だが、アジャイル開発の商業的なアンチパターンを紹介しようと思う。

このようなケースは、技術者の生活も人生も台無しにする。フリーランスエンジニアはアジャイル開発の案件獲得前の打ち合わせ時に面談を通して、こういった状態に陥る恐れのある人柄や組織文化ではないか確認すると良いだろう。

スクラムにおけるアンチパターン:

1.短期的な利益優先
品質よりも納期を優先
技術的負債を無視して機能追加を急ぐ
長期的な保守性や拡張性を犠牲にする

2.コスト削減への過度な圧力
チーム規模の最適化を無視した人員削減
必要なツールや環境への投資不足
トレーニングやスキル開発の機会削減

3.契約形態の問題
固定価格契約による柔軟性の欠如
成果物の定義が不明確な契約
アジャイルに適さない契約条件

4.ステークホルダーの商業的プレッシャー
非現実的な期待や要求
スプリントの途中での優先順位の頻繁な変更
ROI(投資利益率)の短期的な最大化要求

5.組織文化の不適合
従来型のウォーターフォール思考の残存
短期的な業績評価制度
リスク回避的な意思決定

これらの経営サイドの観点から生まれる商業的な要因は、スクラムの本来の価値や原則を損なう可能性があり、プロジェクトの成功を妨げる要因となりかねない。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?