Help us understand the problem. What is going on with this article?

Lombokを使っているときにJacocoのカバレッジから自動生成分を除外する方法

More than 1 year has passed since last update.

概要

このエントリでは、ソースコードを生成してくれるLombokを使っている前提で、カバレッジ計測ツールのJacocoでのカバレッジを上げていく方法について、サンプルを用意して紹介します。

このエントリの目的は、Lombokを使ってなるべくコードを書かないで、かつJacocoでのカバレッジを上げていくときの、とっかかりの参考となることを目的としています。それぞれのツールの全ての機能を解説するものではありません。

結論

結論から言うと、Jacoco0.8.0(2018/1/2リリース)以上を使って、lombok側で「lombok.addLombokGeneratedAnnotation = true」を指定することで、自動生成されたコードをカバレッジ検査対象から除外しましょう、というのが大きなポイントです。

参考エントリ

Qiita内では、Lombok, Jacocoについて、

たくさん(2018/5/26時点)ありますが、下記が参考になります。

- Jacoco 0.8.0でJavaコードカバレッジをより快適に

初期サンプル

サンプルとして、Spring Bootで、REST Controller越しに簡単なエンティティを格納したり表示したりするものを作りました。

こんな感じの動作をするものです。

(登録)
$ curl -H 'Content-Type:application/json' -d '{"intField":123}' http://localhost:8080

(参照)
$ curl http://localhost:8080/ 
[{"id":"2815cfc6-f908-4c7d-a20b-b2c52953555f","intField":123,"longField":0,"stringField":null,"charField"

(1件詳細)
$ curl http://localhost:8080/2815cfc6-f908-4c7d-a20b-b2c52953555f
{"id":"2815cfc6-f908-4c7d-a20b-b2c52953555f","intField":123,"longField":0,"stringField":null,"charField":"\u0000","byteField":0}hirokis-M

初期状態のリポジトリに格納しています。

この版では、lombokのお便利機能を使わず実装しています。

$ ./gradlew test
$ ./gradlew jacocoTestReport

カバレッジはこんな感じです。
スクリーンショット 2018-05-26 14.39.51.png

lombok適用

@ Data

適用前のSampleEntity
SampleEntity.java
public class SampleEntity {
    private SampleId id;
    private int intField;
    private long longField;
    private String stringField;
    private char charField;
    private byte byteField;
    /**
     * @return the id
     */
    public SampleId getId() {
        return id;
    }
    /**
     * @param id the id to set
     */
    public void setId(SampleId id) {
        this.id = id;
    }
    /**
     * @return the intField
     */
    public int getIntField() {
        return intField;
    }
    /**
     * @param intField the intField to set
     */
    public void setIntField(int intField) {
        this.intField = intField;
    }
    /**
     * @return the longField
     */
    public long getLongField() {
        return longField;
    }
    /**
     * @param longField the longField to set
     */
    public void setLongField(long longField) {
        this.longField = longField;
    }
    /**
     * @return the stringField
     */
    public String getStringField() {
        return stringField;
    }
    /**
     * @param stringField the stringField to set
     */
    public void setStringField(String stringField) {
        this.stringField = stringField;
    }
    /**
     * @return the charField
     */
    public char getCharField() {
        return charField;
    }
    /**
     * @param charField the charField to set
     */
    public void setCharField(char charField) {
        this.charField = charField;
    }
    /**
     * @return the byteField
     */
    public byte getByteField() {
        return byteField;
    }
    /**
     * @param byteField the byteField to set
     */
    public void setByteField(byte byteField) {
        this.byteField = byteField;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + byteField;
        result = prime * result + charField;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        result = prime * result + intField;
        result = prime * result + (int) (longField ^ (longField >>> 32));
        result = prime * result
                + ((stringField == null) ? 0 : stringField.hashCode());
        return result;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SampleEntity other = (SampleEntity) obj;
        if (byteField != other.byteField)
            return false;
        if (charField != other.charField)
            return false;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        if (intField != other.intField)
            return false;
        if (longField != other.longField)
            return false;
        if (stringField == null) {
            if (other.stringField != null)
                return false;
        } else if (!stringField.equals(other.stringField))
            return false;
        return true;
    }
    /**
     * @param id
     * @param intField
     * @param longField
     * @param stringField
     * @param charField
     * @param byteField
     */
    public SampleEntity(SampleId id, int intField, long longField,
            String stringField, char charField, byte byteField) {
        super();
        this.id = id;
        this.intField = intField;
        this.longField = longField;
        this.stringField = stringField;
        this.charField = charField;
        this.byteField = byteField;
    }

    public SampleEntity() {
        super();
    };

}

こうなります。

Java:SampleEntity.java
@Data
public class SampleEntity {
private SampleId id;
private int intField;
private long longField;
private String stringField;
private char charField;
private byte byteField;
}

lombokプラグインが適用されたIDE(このエントリではEclipse)で確認すると、setter, getter, toString, hashCodeが生成されていることが確認できます。

スクリーンショット 2018-05-26 14.53.59.png

@ RequiredArgsConstructor

Springの@Autowiredでフィールドをインジェクションしていると、テストなどので直接差し替えが面倒になったり、使っているものが多くなってきたときに場所をとったりということがあるので、コンストラクタでインジェクションする形に変えてみます。

適用前のSampleApi.javaが
SampleApi.java
@RestController
@RequestMapping("/")
public class SampleApi {
    @Autowired
    private SampleFacade sampleFacade;

こうなります。

フィールドがfinalになっているところに注意です。
SampleApi.java
@RestController
@RequestMapping("/")
@RequiredArgsConstructor
public class SampleApi {
    private final SampleFacade sampleFacade;    

ここまでで、jacocoReportはこんな感じになります。

スクリーンショット 2018-05-26 15.10.35.png

ここで、domainパッケージが結構な割合になっているわけですが、中をみれば当然ながらlombokで自動生成されたコードなわけです。lombokの動作を正とする時、生成されたものをテストすることにはあまり意味はありません。

スクリーンショット 2018-05-26 15.07.45.png

このような理由で、Lombokで生成されたコードをテストから除外する指定をしてみます。
プロジェクトのディレクトリに下記のファイルを置き、もう一度テストを実施してみます。

lombok.config
lombok.addLombokGeneratedAnnotation = true 
$ ./gradlew clean
$ ./gradlew test
$ ./gradlew jacocoTestReport 

で、カバレッジがこのように変わります。テストコードをまだ書いていないので赤い状況は変わりまえんが、domainのところが大きく減り、全体のコードのInstructionが、731から170に大きく減っています。

スクリーンショット 2018-05-26 15.17.31.png

lombok.configに指定可能な値の説明は、lombokの公式サイトに説明があります。

ここまでのものをGitHubにv0.2として置きました。

テストを追加

上記のものに、単体テストで、とりあえず1パスは通るようにしてカバレッジを100%にした状態が、GitHub上のサンプルv0.3になります。

スクリーンショット 2018-05-26 17.09.12.png

(参考)これで、除外設定をもとにもどす(指定しない)と、このような状況になります。

スクリーンショット 2018-05-26 17.13.05.png

まとめ

このエントリでは、lombokで自動生成されたコードをカバレッジの計測から除外する方法を確認し、実際に動作するところまでをサンプルコードを通して示しました。

lombokの機能は本エントリのサンプルで使った以外にも本家のサイトでいろいろ紹介されているので、別の機会に利用例を紹介したいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした