AI時代のSEに残る仕事は「責任を持つ」ことかもしれない 〜単体テストを題材に〜
投稿内容は私個人の見解に基づくものであり、所属企業・部門見解を代表するものではありません。
はじめに
AI にコード生成を任せる場面がかなり増えてきました。実装だけを見ると、かなりの部分を AI が肩代わりできるようになっています。
その一方で、人間に残る仕事は何かと考えると、私は「責任を持つこと」がかなり大きいのではないかと思っています。
今回はその話を、単体テストを題材に考えてみます。
この記事では単体テストの厳密な定義よりも、AI と人間の役割分担を考えることを目的にします。
題材のコード
今回は、ポイント計算を行う次のクラスを使います。
public class LoyaltyPointCalculator {
/**
* 購入金額とユーザー属性から、今回付与するポイントを計算する
*/
public int calculatePoints(long amount, String memberStatus, boolean isCampaignDay, boolean hasCoupon) {
if (amount < 0) {
throw new IllegalArgumentException("金額が負の値です");
}
double rate = switch (memberStatus) {
case "PLATINUM" -> 0.10; // 10%
case "GOLD" -> 0.05; // 5%
default -> 0.01; // 1%
};
double finalRate = (isCampaignDay && !"PLATINUM".equals(memberStatus)) ? rate * 2 : rate;
long points = Math.round(amount * finalRate);
if (hasCoupon && amount >= 1000) {
points += 100;
}
return (int) Math.min(points, 5000);
}
}
仕様として読み取れそうなのは、たとえば次のような内容です。
- 会員ランクごとに還元率が違う
- キャンペーン日は一部会員だけ倍率がかかる
- 1000 円以上でクーポン加算がある
- 付与ポイントには上限がある
- 負の値の金額は例外にする
実験
同じクラスに対して、AI に 2 通りの頼み方をしました。
IDEはIntelliJ IDEA、モデルはClaude Sonnet 4.6です。
1. 雑に依頼する
プロンプトはこれだけです。
LoyaltyPointCalculatorクラスに単体テストを被せて。
この場合、AI はかなり広めに解釈して、会員ランク、キャンペーン日、クーポン、上限、境界値、異常系、複合条件まで含めた長いテストを生成しました。
実際に生成されたコードは250行近くありましたが、抜粋すると次のようになります。
import文は省略しています。
@DisplayName("LoyaltyPointCalculator テスト")
class LoyaltyPointCalculatorTest {
private LoyaltyPointCalculator calculator;
@BeforeEach
void setUp() {
calculator = new LoyaltyPointCalculator();
}
@Nested
@DisplayName("会員ランク別のポイントレート")
class MemberStatusRateTest {
@Test
@DisplayName("REGULAR会員は購入金額の1%がポイントになる")
void regularMemberGets1Percent() {
assertEquals(100, calculator.calculatePoints(10000, "REGULAR", false, false));
}
@Test
@DisplayName("GOLD会員は購入金額の5%がポイントになる")
void goldMemberGets5Percent() {
assertEquals(500, calculator.calculatePoints(10000, "GOLD", false, false));
}
@Test
@DisplayName("PLATINUM会員は購入金額の10%がポイントになる")
void platinumMemberGets10Percent() {
assertEquals(1000, calculator.calculatePoints(10000, "PLATINUM", false, false));
}
@Test
@DisplayName("未知の会員ランクはREGULARと同様に1%になる")
void unknownStatusGets1Percent() {
assertEquals(100, calculator.calculatePoints(10000, "SILVER", false, false));
}
}
@Nested
@DisplayName("異常系: 不正な入力値")
class ExceptionTest {
@Test
@DisplayName("購入金額が負の値の場合はIllegalArgumentExceptionをスローする")
void negativeAmountThrowsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class,
() -> calculator.calculatePoints(-1, "REGULAR", false, false));
}
}
}
良かった点は、観点が広いことです。少なくともコードの分岐はかなり丁寧に拾ってくれます。
ただ、気になった点もありました。たとえば「未知の会員ランクは REGULAR と同様に 1%」というテストは、コード上は正しいです。が、仕様として意図されているものかどうかは不明確です。
そして、仕様が曖昧な状態で250行近いテストをレビューする側は、確認観点が増えて負荷が高くなります。
2. テストケースを明示して依頼する
次は、先に私がテストケースを整理してから依頼しました。
test_case.csvに以下のテストケースを記載した。LoyaltyPointCalculatorクラスに単体テストを被せて。
与えたケースは次の 8 件です。
| ケースID | 購入金額 | 会員ランク | キャンペーン日 | クーポン | 期待されるポイント |
|---|---|---|---|---|---|
| TC-01 | 1,000 | REGULAR | FALSE | FALSE | 10 |
| TC-02 | 1,000 | GOLD | FALSE | FALSE | 50 |
| TC-03 | 1,000 | GOLD | TRUE | FALSE | 100 |
| TC-04 | 1,000 | PLATINUM | TRUE | FALSE | 100 |
| TC-05 | 999 | REGULAR | FALSE | TRUE | 10 |
| TC-06 | 1,000 | REGULAR | FALSE | TRUE | 110 |
| TC-07 | 100,000 | PLATINUM | FALSE | FALSE | 5,000 |
| TC-08 | -1 | ANY | - | - | Error |
この場合、AI はそのテストケースを素直にコード化した、かなりシンプルなテストを返しました。
実際の生成コードは次の通りです。これは全コードです。
import文は省略しています。
@DisplayName("LoyaltyPointCalculator 単体テスト")
class LoyaltyPointCalculatorTest {
private final LoyaltyPointCalculator calculator = new LoyaltyPointCalculator();
@DisplayName("正常系: ポイント計算")
@ParameterizedTest(name = "[{index}] {0}: 購入金額={1}, ランク={2}, キャンペーン={3}, クーポン={4} → 期待ポイント={5}")
@CsvSource({
"TC-01, 1000, REGULAR, false, false, 10",
"TC-02, 1000, GOLD, false, false, 50",
"TC-03, 1000, GOLD, true, false, 100",
"TC-04, 1000, PLATINUM, true, false, 100",
"TC-05, 999, REGULAR, false, true, 10",
"TC-06, 1000, REGULAR, false, true, 110",
"TC-07, 100000, PLATINUM, false, false, 5000"
})
void testCalculatePoints(String caseId, long amount, String memberStatus,
boolean isCampaignDay, boolean hasCoupon, int expectedPoints) {
int actual = calculator.calculatePoints(amount, memberStatus, isCampaignDay, hasCoupon);
assertEquals(expectedPoints, actual, caseId + " のポイント計算結果が一致しません");
}
@Test
@DisplayName("異常系: 負の購入金額は IllegalArgumentException")
void TC_08_negativeAmountThrowsException() {
assertThrows(IllegalArgumentException.class,
() -> calculator.calculatePoints(-1, "REGULAR", false, false),
"TC-08: 負の金額で IllegalArgumentException がスローされるべきです"
);
}
}
こちらの良かった点は、何を確認したいテストなのかが明確なことです。保守もしやすいと思います。
一方で、当然ながら与えたケースしか検証しません。つまり、網羅性の責任は人間側に残ります。
このケース群では、コード上の主要な分岐をすべて確認できました。
比較して見えたこと
この 2 つを比べると、AI は依頼内容にかなり忠実に応答してくれます。だからこそ、最終的に「それを仕様として採用するか」は人間の判断が必要です。
- 曖昧に頼めば、AI はコードを読んでそれっぽく広く補完する
- 具体的に頼めば、AI は与えられたケースを正確にコード化する
どちらも便利です。ただし、「このテストが仕様として妥当か」を最終判断する役割は、今のところ人間に残ります。
たとえば上司に「単体テストを作って」と言われたとき、私は 2 つ目のほうが自信を持って提出しやすいです。
理由はシンプルで、自分が責任を持てるからです。
1 つ目は、たしかに広く確認できています。でも、その中には「コードはそうなっているが、本当に仕様なのか分からないもの」も混ざります。そこに責任を持つのは難しいです。
2 つ目は、人間が仕様として確認したいケースを定義し、その実装だけを AI に任せています。責任の所在がはっきりしています。
まとめ
今回の実験で感じたことは次の通りです。
- AI はコードを読んで、それらしいテストをかなり速く作れる
- ただし、仕様の妥当性や網羅性を引き受けるのは人間
- 人間の仕事は減るというより、「どこに責任を持つか」がより重要になる
AI に実装を任せること自体は、もう特別なことではないと思います。
そのうえで、人間がやるべきなのは「何を仕様とみなすのか」「どこまでを保証するのか」を決め、その判断に責任を持つことではないでしょうか。
みなさんなら、どちらのテストを提出しますか。