Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

FindBugsのバグパターンをMavenでビルド&テストする

以前の記事FindBugsバグパターンの実装方法を試行錯誤している最中に作成したものでしたが、今回はその結果どのような開発手法に行き着いたかをまとめます。Mavenベースです。

前提

  • findbugs.xmlsrc/main/resourcesに置いてしまうと単体テスト実行時にエラーが発生してしまう(前回記事参照)
  • テストが面倒
  • Mavenプラグイン、Antタスク、SonarQubeなど様々な利用方法があり得る

拡張すべきクラスについては前回記事を参照してください。

ビルドのTips

メタファイルをどうpackageするか

findbugs.xmlsrc/main/resourcesに置いてしまうことによるエラーを回避するために、メタファイルをsrc/main/metaファイルに保存してprepare-packageフェーズにリソースに追加するという手法を取ります。プロファイルを利用することもなくシンプルな解決になります。

Mavenを実行するたびにcleanを実行しなければならないという制約は付きますが、Detector用プロジェクトは基本的に小さいものになるはずですので問題にはならないでしょう。プラグイン設定は以下のようになります。

      <plugin>
        <!--
          Copy meta files to default outputDirectory at "prepare-package" phase.

          Because of findbugs specification, an edu.umd.cs.findbugs.PluginDoesntContainMetadataException
          instance would be thrown at "test" phase if we put these meta files on '/src/main/resources'.
          We have to copy these meta files after testing.

          See "How to build FindBugs plugin with Maven" thread in FindBugs mailing list
          (findbugs-discuss@cs.umd.edu) to get detail and other solution.

          Note: You should execute "clean" phase before you execute "test" phase.
        -->
        <artifactId>maven-resources-plugin</artifactId>
        <executions>
          <execution>
            <phase>prepare-package</phase>
            <goals>
              <goal>copy-resources</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.outputDirectory}</outputDirectory>
              <resources>
                <resource>
                  <directory>src/main/meta</directory>
                </resource>
              </resources>
            </configuration>
          </execution>
        </executions>
      </plugin>

依存ライブラリをパッケージする

findbugs-maven-pluginだけでなくSonarQubeなどでの利用も検討している場合、依存ライブラリも同梱してしまうと便利です。私はmaven-shade-pluginでこれを実現しています。

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <createDependencyReducedPom>false</createDependencyReducedPom>
            </configuration>
          </execution>
        </executions>
      </plugin>

テストのTips

テストには大きく分けて2通りの方法を採用しています。FindBugs本体が提供する機構はドキュメントが無いため現時点では使わず、外部ライブラリに頼った手法のみを利用しています。

test-driven-detectors4findbugs による単体テスト

youdevise/test-driven-detectors4findbugs @ GitHubはDetectorをtest-drivenな開発手法で作成できる優れたライブラリです。FindBugs 2.0.3にはまだ対応できていませんが、多くの場合問題にはならないでしょう。

使い方は非常にシンプルで、テスト用のBugReporterをDetectorに渡してからassertXxxメソッドでバグが発見されるかどうかを調べるだけです。

// quoted from https://github.com/WorksApplications/findbugs-plugin/blob/1c1930cacd0fe2aed9b41633b9b72d6a9b50e07e/src/test/java/jp/co/worksap/oss/findbugs/jsr305/BrokenImmutableClassDetectorTest.java
public class BrokenImmutableClassDetectorTest {

    private BrokenImmutableClassDetector detector;
    private BugReporter bugReporter;

    @Before
    public void setup() {
        bugReporter = bugReporterForTesting();
        detector = new BrokenImmutableClassDetector(bugReporter);
    }

    @Test
    public void testEnumIsImmutable() throws Exception {
        assertNoBugsReported(When.class, detector, bugReporter);
    }

    @Test
    public void testMutableClass() throws Exception {
        assertBugReported(MutableClass.class, detector, bugReporter, ofType("IMMUTABLE_CLASS_SHOULD_BE_FINAL"));
        assertBugReported(MutableClass.class, detector, bugReporter, ofType("BROKEN_IMMUTABILITY"));
    }
}

この方法は簡単かつ充分なテストが行えるため、まず最初に導入したいところです。ただしこのライブラリはfindbugs.xmlのようなメタファイルは参照しないため、メタファイルと実装の不整合を見つけることはできません。実際に実行を試せる環境を別途用意するか、次に述べる統合テストを併用する必要があります。

なお依存関係は以下のようになります。

    <dependency>
      <groupId>com.google.code.findbugs</groupId>
      <artifactId>findbugs</artifactId>
      <version>2.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-library</artifactId>
      <version>1.3</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.youdevise</groupId>
      <artifactId>test-driven-detectors4findbugs</artifactId>
      <version>0.2.1</version>
      <scope>test</scope>
    </dependency>

実際にFindBugsを実行した結果を解析

eller86/findbugs-slf4jで利用している方法です。Mavenのマルチモジュールを利用したやや複雑な構成です。

まずはDetector実装を含むサブモジュールを作成します。次にDetectorによる解析を行うためのクラスファイルをsrc/main/javaに持つサブモジュールを作成し、pre-integration-testフェーズでfindbugs-maven-pluginを実行することで、その結果をintegration-testフェーズで解析・照合することが可能になります。 解析用サブモジュールをDetectorサブモジュールにtestスコープで依存させることで、mvn clean install実行時にfindbugs-maven-pluginが常に新しい実装を利用することを保証できます。

この手法は実際にfindbugs-maven-pluginを利用するため、FindBugsプラグインが期待通りに動作すること、すなわちメタファイルの整合性も含めた動作保証が可能になります。XMLパーサを用意するなど実施コストは高くなりますが、この動作保証を手動で行う手間を考えるとペイすると言えるでしょう。

プラグイン設定は以下のようになります。src/test/javaにあるテストケースをintegration-testフェーズで実行するためにmaven-surefire-pluginに2つのexecutionを設定しています。

      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>findbugs-maven-plugin</artifactId>
        <version>2.5.2</version>
        <executions>
          <execution>
            <phase>pre-integration-test</phase>
            <goals>
              <goal>findbugs</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <threshold>Low</threshold>
          <plugins>
            <plugin>
              <groupId>my.groupId</groupId>
              <artifactId>my.detector</artifactId>
              <version>0.0.1-SNAPSHOT</version>
            </plugin>
          </plugins>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.12</version>
        <executions>
          <execution>
            <id>default-test</id>
            <phase>test</phase>
            <configuration>
              <skip>true</skip><!-- all test case should run at integration-test phase -->
            </configuration>
          </execution>
          <execution>
            <id>default-integration-test</id>
            <phase>integration-test</phase>
            <goals><goal>test</goal></goals>
          </execution>
        </executions>
      </plugin>

参考資料