APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

【GitLab Duo Chat】JavaのテストコードをAI生成してみる(正規表現、Mock)

クラウド事業部CI/CDサービスメニューチームの菅家です。
今回はGitLab16.11でGAとなったGitLab Duo Chatを使ってJavaのテストコード生成を試していきたいと思います。

GitLab Duo Chatの概要についてはこちらの記事で紹介しております。
techblog.ap-com.co.jp

目次

GitLab Duo Chatを使用するには?(JetBrains IDEs)

GitLab Duo Chatを使用するにはWebIDEを使用するか、
ご自身のIDEで実行する場合はIDEの設定が必要になります。

今回はIntelliJ(JetBrains IDEs)より試してみます。
IntelliJで使用するにはGitLab Duoプラグインの導入が必要になります。 導入手順はこちらを参照。
techblog.ap-com.co.jp

docs.gitlab.com

IntelliJのプラグイン設定も項目が増えています。
デフォルトのGitLab Duo Chatにチェックを入れた状態で設定します。


設定後、画面右にGitLab Duo Chatのアイコンが表示されることを確認します。
ここをクリックして、表示されたチャット画面を使ってテストコードを生成していきます。
テストを生成したいソースコードを選択した状態でチャット欄に「/tests」と入力することで生成されます。

もしくは、右クリックメニューも増えているためコードを選択して右クリック「GitLab Duo Chat>Generate Tests」からも実行できます。

余談として、パーソナルアクセストークン生成の際に「ai_features」の項目が増えていました。
詳細なドキュメントが見つからなかったので、実際に検証したところから記載しますが、GitLab Duo Chatを使用する場合はapiのチェックのみで問題なさそうです。

サンプルコード

概要

サンプルコードとして以下を用意しました。
簡単なユーザー登録処理を想定して作成しました。

・引数・戻り値がある
・処理が分岐する
・処理内で他クラスを呼び出す(Mockを期待)
これらを実装するよう作成しました。

クラス名 説明
Member 会員情報を格納するVOクラス(名前、会員番号、会員ランク)
AddMemberResult 会員登録処理結果格納クラス
MemberManager 会員情報処理クラス(会員登録処理、特別会員の判定処理)、MemberValidatorを持つ
MemberValidator 会員情報のバリデーションクラス(Memberクラスの各フィールドの値が有効か)
RankEnum 会員のランクを表すEnum(ブロンズ、シルバー、ゴールド、プラチナ)
クラス図

簡単に作成してみました。
コンストラクタ、Getter/Setter、staticやfinal修飾子は省略しています。

ソースコード詳細

Member

package apc;

public class Member {
    private String name;
    private final int number;
    private RankEnum rank;

    Member(String name, int number, RankEnum rank){
        this.name = name;
        this.number = number;
        this.rank =rank;
    }

    public String getName() {
        return name;
    }

    public int getNumber() {
        return number;
    }

    public RankEnum getRank() {
        return rank;
    }
    public void setName(String name) {
        this.name = name;
    }

    public void setRank(RankEnum rank) {
        this.rank = rank;
    }
}

AddMemberResult

package apc;

public class AddMemberResult {
    private String httpStatusCode;
    private Member member;

    public AddMemberResult(String httpStatusCode, Member member){
        this.httpStatusCode = httpStatusCode;
        this.member = member;
    }

    public String getHttpStatusCode() {
        return httpStatusCode;
    }

    public void setHttpStatusCode(String httpStatusCode) {
        this.httpStatusCode = httpStatusCode;
    }

    public Member getMember() {
        return member;
    }

    public void setMember(Member member) {
        this.member = member;
    }
}

MemberManager

package apc;

public class MemberManager {
    MemberValidator validator;

    public MemberManager(MemberValidator validator) {
        this.validator = validator;
    }

    public AddMemberResult addMember(Member member) {
        if(this.validator.validate(member))
            return new AddMemberResult("400", member);

        return new AddMemberResult("200", member);
    }

    public boolean isExtraMemberRank(Member member){
        if (member.getRank() == RankEnum.GOLD) return true;
        if (member.getRank() == RankEnum.PLATINUM) return true;

        return false;
    }

}

MemberValidator

package apc;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class MemberValidator {
    private static final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z]{1,30}$");

    public boolean validate(Member member){
        if (!NAME_PATTERN.matcher(member.getName()).matches())
            return false;
        return true;
    }
}

RankEnum

package apc;

public enum RankEnum {
    BRONZE ,SILVER, GOLD ,PLATINUM
}

テストコードを生成する

MemberValidatorとMemberManagerに絞って検証します。

MemberValidatorクラス

validateメソッド(正規表現による判定)

Memberクラスの値が正しいかを検証する処理です。
正規表現を使用して判定し、会員の名前が1文字以上、30文以下の英字の場合は正しい値としてtrue、それ以外は無効な値としてfalseを返却します。

コード

    private final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z]{1,30}$");

    public boolean validate(Member member){
        if (!NAME_PATTERN.matcher(member.getName()).matches())
            return false;
        return true;
    }

回答

To test the validate method of the MemberValidator class, we can create a test class with multiple test cases. Here's an example using JUnit:

import apc.Member;
import apc.MemberValidator;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class MemberValidatorTest {
    private MemberValidator validator;

    @Before
    public void setUp() {
        validator = new MemberValidator();
    }

    @Test
    public void testValidateShouldReturnTrueForValidName() {
        Member member = new Member("JohnDoe");
        boolean isValid = validator.validate(member);
        assertTrue(isValid);
    }

    @Test
    public void testValidateShouldReturnFalseForEmptyName() {
        Member member = new Member("");
        boolean isValid = validator.validate(member);
        assertFalse(isValid);
    }

    @Test
    public void testValidateShouldReturnFalseForNameWithNumbers() {
        Member member = new Member("John123");
        boolean isValid = validator.validate(member);
        assertFalse(isValid);
    }

    @Test
    public void testValidateShouldReturnFalseForNameWithSpecialCharacters() {
        Member member = new Member("John@Doe");
        boolean isValid = validator.validate(member);
        assertFalse(isValid);
    }

    @Test
    public void testValidateShouldReturnFalseForNameLongerThan30Characters() {
        Member member = new Member("JohnDoeWithAVeryLongNameIndeed");
        boolean isValid = validator.validate(member);
        assertFalse(isValid);
    }
}

In this test class, we have the following test cases:

testValidateShouldReturnTrueForValidName: Tests that the validate method returns true for a valid name (only alphabetic characters and length between 1 and 30).
testValidateShouldReturnFalseForEmptyName: Tests that the validate method returns false for an empty name.
testValidateShouldReturnFalseForNameWithNumbers: Tests that the validate method returns false for a name containing numbers.
testValidateShouldReturnFalseForNameWithSpecialCharacters: Tests that the validate method returns false for a name containing special characters.
testValidateShouldReturnFalseForNameLongerThan30Characters: Tests that the validate method returns false for a name longer than 30 characters.
These tests cover various scenarios for the validate method and ensure that it behaves as expected.

充実した回答が返ってきております。

回答を抜粋すると、

testValidateShouldReturnTrueForValidName: Tests that the validate method returns true for a valid name (only alphabetic characters and length between 1 and 30).

日本語訳

testValidateShouldReturnTrueForValidName:メソッドが有効な名前 (アルファベット文字と 1 ~ 30 の間の長さのみ) に対して true を返すかどうかをテストします。

となっており、正規表現に対しての解釈もできています。

有効な値の時、文字数が足りない時・多すぎる時、無効な文字が入った時などパターンがあると嬉しいと思っていたところ。
最後の「testValidateShouldReturnFalseForNameLongerThan30Characters」はテストの値が30文字ぴったりのため、生成されたテストメソッド名の意図に反してTrueになってしまいます。
このテストの値に関しては31文字に修正が必要ですが、文字が多すぎる時にFalseとなるテストを組みたかった、というこちらの意図はくみ取ってくれています。
正規表現も読み取ってくれるのは驚きでした。

おまけ:code explainedで正規表現の説明をしてくれるのか検証

前述の正規表現の流れでGitLab Duo Chatにはソースコードの説明(code explained)機能もあるので、正規表現部分を読んでくれるか確認します。(/explain)

The selected code:

private static final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z]{1,30}$");
This line defines a private static final variable named NAME_PATTERN of type Pattern. The Pattern class in Java is used for defining a regular expression pattern that can be used for matching and manipulating strings.

The regular expression "^[a-zA-Z]{1,30}$" is compiled into a Pattern object using the Pattern.compile() method. Let's break down the regular expression:

^ represents the start of the string.
[a-zA-Z] is a character class that matches any uppercase or lowercase letter from A to Z.
{1,30} is a quantifier that specifies the pattern should match between 1 and 30 occurrences of the preceding character class [a-zA-Z].
$ represents the end of the string.
So, the regular expression ^[a-zA-Z]{1,30}$ matches any string that:

Starts with a letter (uppercase or lowercase).
Contains only letters (uppercase or lowercase).
Has a length between 1 and 30 characters.
This pattern is likely used to validate the name of a Member object, ensuring that it consists of only alphabetic characters and has a length between 1 and 30 characters.

Pattern.compileの説明に加えて、正規表現の説明もしてくれます。
正規表現を読むときだけではなく、自分が書いた正規表現が正しいか確認するのにも使えそうでとても便利です。

他にも少し複雑な条件で試してみました。完全に正しい答えが返るわけではないのですが、使われている正規表現の文字の意味を答えてくれるため、検索の手間が省けます。

①private static final Pattern TEST_PATTERN = Pattern.compile("^[a-zA-Z].+?\btest\b$");
英字1文字以上かつ一番初めに見つかった区切りまで+testが区切り文字前後にある。

The code you selected is a regular expression pattern defined as a constant in the MemberValidator class. Here's an explanation of the pattern:

private static final Pattern TEST_PATTERN = Pattern.compile("^[a-zA-Z].+?\\btest\\b$");
This regular expression pattern is used to match strings that start with a single alphabetic character (either uppercase or lowercase), followed by any characters (except newline), and ending with the word "test" as a whole word.

Here's a breakdown of the pattern:

^ - Matches the start of the string.
[a-zA-Z] - Matches any single alphabetic character (either uppercase or lowercase).
.+? - Matches any character (except newline) one or more times, but as few times as possible (non-greedy).
\\b - Matches a word boundary (position between a word character and a non-word character).
test - Matches the literal string "test".
\\b - Matches another word boundary.
$ - Matches the end of the string.
So, this pattern would match strings like "atest", "bthisIsAtest", "cSomeOthertest", but it would not match strings like "test123" (because it doesn't start with an alphabetic character), "123test" (same reason), or "atestString" (because "test" is not a whole word).

Regular expressions are commonly used for pattern matching and text manipulation in various programming languages, including Java.


②private static final Pattern TEST_PATTERN_2 = Pattern.compile("^(?!.(abc)).");
abcを含まない行。

The selected code:

private static final Pattern TEST_PATTERN_2 = Pattern.compile("^(?!.*(abc)).*");
This line defines a static final Pattern object named TEST_PATTERN_2. The Pattern class in Java is used for defining a regular expression pattern that can be used for matching and manipulating strings.

The regular expression pattern "^(?!.*(abc)).*" is compiled and stored in the TEST_PATTERN_2 object. Let's break down this regular expression:

^ - Matches the start of the string.
(?!.*(abc)) - This is a negative lookahead assertion. It ensures that the pattern does not match any string that contains the substring "abc" anywhere within it.
.* - Matches any character (except newline) zero or more times.
(abc) - Matches the literal string "abc".
?! - Negative lookahead assertion, which asserts that the pattern inside the parentheses should not match the input string at the current position.
.* - Matches any character (except newline) zero or more times.
In summary, the TEST_PATTERN_2 pattern will match any string that does not contain the substring "abc". It can be used to validate or filter strings based on this condition.

For example, if you have a Matcher object m created from TEST_PATTERN_2, then m.matches("hello") will return true, but m.matches("abcdef") will return false.

MemberManagerクラス

会員(Member)クラスに対して処理を行うクラスです。

addMemberメソッド(Mock)

新規会員の登録処理を行う、フリをする処理です。
MemberValidatorクラスのvalidateメソッドを呼び出し、会員の情報が有効であればAddMemberResultにステータス200、無効なら400を入れて返します。
他クラスを呼ぶ処理とすることで、Mockを使うことを期待したメソッドとなっています。

コード

    public AddMemberResult addMember(Member member) {
        if(this.validator.validate(member))
            return new AddMemberResult("400", member);

        return new AddMemberResult("200", member);
    }

回答

To test the addMember method, we can create a mock implementation of the MemberValidator interface and test the different scenarios based on the return value of the validate method. Here's an example of how we can write tests for the addMember method using JUnit:

import apc.Member;
import apc.MemberManager;
import apc.MemberValidator;
import apc.RankEnum;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;

public class MemberManagerTest {

    @Mock
    private MemberValidator mockValidator;

    private MemberManager memberManager;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        memberManager = new MemberManager(mockValidator);
    }

    @Test
    public void testAddMember_ValidMember() {
        Member member = new Member("John Doe", RankEnum.REGULAR);
        when(mockValidator.validate(member)).thenReturn(true);

        AddMemberResult result = memberManager.addMember(member);

        assertEquals("400", result.getCode());
        assertEquals(member, result.getMember());
    }

    @Test
    public void testAddMember_InvalidMember() {
        Member member = new Member("Jane Smith", RankEnum.GOLD);
        when(mockValidator.validate(member)).thenReturn(false);

        AddMemberResult result = memberManager.addMember(member);

        assertEquals("200", result.getCode());
        assertEquals(member, result.getMember());
    }
}
In this example, we create a mock implementation of the MemberValidator interface using Mockito. We then create an instance of the MemberManager class and inject the mock validator into it.

The testAddMember_ValidMember method tests the scenario where the validate method of the MemberValidator returns true. We set up the mock validator to return true for the given Member object, and then we call the addMember method on the MemberManager instance. We assert that the returned AddMemberResult object has the code "400" and the same Member object that was passed in.

The testAddMember_InvalidMember method tests the scenario where the validate method of the MemberValidator returns false. We set up the mock validator to return false for the given Member object, and then we call the addMember method on the MemberManager instance. We assert that the returned AddMemberResult object has the code "200" and the same Member object that was passed in.

Note that this example assumes the existence of the Member and RankEnum classes, as well as the AddMemberResult class with getCode and getMember methods.

なんと、Mockを使ってくれています。
ふとした時にMock使ったテストってどう作るんだっけと、調べることがあり、1から作るのはなかなか大変なところ、GitLab Duo ChatであればIDE上で何秒か待てばで回答してくれるところが本当に便利です。

isExtraMemberRankメソッド

こちらは単純な分岐となります。
会員のランクがブロンズ、シルバー、ゴールド、プラチナとありますが、ゴールドとプラチナの場合に特別会員としてtrue、それ以外は通常会員としてfalseを返却するメソッドとなっています。

コード

    public boolean isExtraMemberRank(Member member){
        if (member.getRank() == RankEnum.GOLD) return true;
        if (member.getRank() == RankEnum.PLATINUM) return true;

        return false;
    }

回答

To test the isExtraMemberRank method, we can create a test class and write test cases for different scenarios. Here's an example of how you can write tests for this method:

import apc.Member;
import apc.RankEnum;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MemberManagerTest {

    @Test
    void testIsExtraMemberRank_GoldMember() {
        // Arrange
        MemberManager memberManager = new MemberManager(null); // Assuming MemberValidator is not needed for this test
        Member member = new Member();
        member.setRank(RankEnum.GOLD);

        // Act
        boolean isExtraMemberRank = memberManager.isExtraMemberRank(member);

        // Assert
        assertTrue(isExtraMemberRank);
    }

    @Test
    void testIsExtraMemberRank_PlatinumMember() {
        // Arrange
        MemberManager memberManager = new MemberManager(null);
        Member member = new Member();
        member.setRank(RankEnum.PLATINUM);

        // Act
        boolean isExtraMemberRank = memberManager.isExtraMemberRank(member);

        // Assert
        assertTrue(isExtraMemberRank);
    }

    @Test
    void testIsExtraMemberRank_SilverMember() {
        // Arrange
        MemberManager memberManager = new MemberManager(null);
        Member member = new Member();
        member.setRank(RankEnum.SILVER);

        // Act
        boolean isExtraMemberRank = memberManager.isExtraMemberRank(member);

        // Assert
        assertFalse(isExtraMemberRank);
    }

    @Test
    void testIsExtraMemberRank_NullMember() {
        // Arrange
        MemberManager memberManager = new MemberManager(null);
        Member member = null;

        // Act
        boolean isExtraMemberRank = memberManager.isExtraMemberRank(member);

        // Assert
        assertFalse(isExtraMemberRank);
    }
}
In this example, we have four test cases:

testIsExtraMemberRank_GoldMember: This test case checks if the isExtraMemberRank method returns true when the member's rank is GOLD.
testIsExtraMemberRank_PlatinumMember: This test case checks if the isExtraMemberRank method returns true when the member's rank is PLATINUM.
testIsExtraMemberRank_SilverMember: This test case checks if the isExtraMemberRank method returns false when the member's rank is SILVER.
testIsExtraMemberRank_NullMember: This test case checks if the isExtraMemberRank method returns false when the member parameter is null.
These tests cover the different scenarios for the isExtraMemberRank method and ensure that it behaves as expected. 

各分岐のテストに加えて、falseとなるパターンをSilver,nullと用意してくれています。

おわりに

開発に携わっていたころに、単体テスト1つ作るのにかなり苦労した記憶があります。
パターンを考えたり、構文を思い出したり、Mock使うことを考えたり、必要に応じて検索したり。
自分が苦労して作っていたところがIDE上から回答してくれるとはとても便利になって良いなと思った次第です。

弊社はGitLabオープンパートナー認定を受けております。 また以下のようにCI/CDの導入を支援するサービスも行っているので、何かご相談したいことがあればお気軽にご連絡ください。

www.ap-com.co.jp