かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その12 )( build.gradle への PMD の導入2 )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その11 )( build.gradle への PMD の導入 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 前回 PMD の設定をしたので、今回は指摘を受けた箇所を修正します。
    • PMD のバージョンも 5.7.0 → 5.8.0 → 5.8.1 へバージョンアップします。

参照したサイト・書籍

目次

  1. PMD を 5.8.0 へ上げて build タスクを実行する
  2. 指摘を受けた箇所を修正する
    1. rulesets/java/comments.xml/CommentRequired
    2. rulesets/java/design.xml
    3. rulesets/java/imports.xml
    4. rulesets/java/logging-java.xml
    5. rulesets/java/naming.xml
    6. rulesets/java/optimizations.xml
    7. rulesets/java/strings.xml
    8. rulesets/java/unnecessary.xml
    9. rulesets/java/unusedcode.xml
  3. clean タスク → Rebuild Project → build タスクを実行する
  4. メモ書き
    1. コンソールに出力されるメッセージがどの RuleSet のどの Rule なのかをどうやって探すのか?
    2. RuleSet を複数適用してメッセージが大量に出力されたらどうするか?
  5. PMD を 5.8.0 → 5.8.1 へ上げて build タスクを実行する
  6. 最後に

手順

PMD を 5.8.0 へ上げて build タスクを実行する

build.gradle の以下の点を変更します。

pmd {
    toolVersion = "5.8.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • toolVersion = "5.7.0"toolVersion = "5.8.0" に変更します。

clean タスク → Rebuild Project → build タスク の順に実行します。

f:id:ksby:20170701065604p:plain

199 PMD rule violations were found. という結果でした。5.7.0 の時は 219 PMD rule violations were found. でしたので、バージョンを上げたら 20 も減りましたね。。。?

減ったところを確認したら、以下のメッセージの部分でした。このメッセージが全て出なくなっているのではなく、一部だけのようです。

  • Private field ... could be made final; it is only initialized in the declaration or constructor.

5.7.0 で指摘を受けた箇所を見てみましたが、final を付けるよう指摘されてもおかしくはないようなところが指摘されなくなっているようでした。出なくなった理由は分かりませんが、そのまま進めます。

指摘を受けた箇所を修正する

rulesets/java/comments.xml/CommentRequired

  • CommentRequired の headerCommentRequirement(headerCommentRequirement Required)は、今回は指摘を受けたクラスに以下のコメントを追加して、指摘が出ないようにだけします。
/**
 * ???
 */

rulesets/java/design.xml

  • UseVarargs(Consider using varargs for methods or constructors which take an array the last parameter.)は、メソッドの最後の引数を Object[] argsObject... args のように変更します。。。と思いましたが、指摘された2箇所はどちらも可変引数ではなく Object[] で受けないといけないところでした。特に null が渡されることがあるところを Object... に変更すると、呼び出し元を null(Object) null に変更しないといけなくなるので、さすがにそれは避けたいと思いました。@SuppressWarnings({"PMD.UseVarargs"}) を付けて指摘されないようにします。
  • UnnecessaryLocalBeforeReturn(Consider simply returning the value vs storing it in local variable ...)は、途中に変数を入れていたところを入れないように修正します。ただし、spring-retry の RetryTemplate#execute を使用している部分はコードを修正せず @SuppressWarnings({"PMD.UnnecessaryLocalBeforeReturn"}) を付加してメッセージが出力されないようにします。
    public MessageConverter messageConverter() {
        // 変更前
        // Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        // return converter;

        // 変更後
        return new Jackson2JsonMessageConverter();
    }
  • UncommentedEmptyConstructor(Document empty constructor)は、指摘された箇所のコンストラクタを削除できないことに気付いたので、pmd-project-rulesets.xml<exclude name="UncommentedEmptyConstructor"/> を追加して、この rule を外すことにします。
  • ImmutableField(Private field ... could be made final; it is only initialized in the declaration or constructor.)は、フィールド変数に final を付けるよう変更します。
  • AccessorMethodGeneration(Avoid autogenerated methods to access private fields and methods of inner / outer classes)はインナークラスからアクセスしているアウタークラスのフィールド変数のアクセス識別子を private → public へ変更します。でも、これはアウタークラスのフィールド変数にアクセスしていること自体が問題なんだろうな、という気がしています。。。
  • PreserveStackTrace(New exception is thrown in catch block, original stack trace may be lost)は、catch 句の中で throw している例外クラスの引数に catch した例外クラスのインスタンスを追加するよう変更します。
  • ClassWithOnlyPrivateConstructorsShouldBeFinal(A class which only has private constructors should be final)は、private コンストラクタのみのクラスに final を付けるよう変更します。
  • AvoidDeeplyNestedIfStmts(Deeply nested if..then statements are hard to read)は、指摘を付けた箇所の if 文内の処理を private メソッドへ分離して if 文のネストの深さを減らすように変更します。

rulesets/java/imports.xml

  • UnnecessaryFullyQualifiedName(Unnecessary use of fully qualified name ... due to existing import ...)は外部のライブラリの存在チェックをしている @ConditionalOnClass(com.fasterxml.jackson.dataformat.xml.XmlMapper.class) のところで指摘されたのですが、ここはパッケージ込みで記述しておきたいので @SuppressWarnings({"PMD.UnnecessaryFullyQualifiedName"}) を付けて指摘されないようにします。

rulesets/java/logging-java.xml

  • LoggerIsNotStaticFinal(The Logger variable declaration does not contain the static and final modifiers)は、Logger の変数に static を付けていなかったので static を付けるよう変更します。

rulesets/java/naming.xml

  • VariableNamingConventions(Variables that are final and static should be all capitals, ... is not all capitals.)は private static final Logger logger = LoggerFactory.getLogger(~.class); の logger を大文字&アンダーバーに変更するよう指摘されることに気付いたので、pmd-project-rulesets.xml<exclude name="VariableNamingConventions"/> を追加して、この rule を外すことにします。
  • SuspiciousConstantFieldName(The field name indicates a constant but its modifiers do not)は定数のフィールド変数に final を付けていなかったので final を付けるよう変更します。

rulesets/java/optimizations.xml

  • PrematureDeclaration(Avoid declaring a variable if it is unreferenced before a possible exit point.)は、変数を定義する位置と最初に使用している位置が離れていたので、定義する位置を使用する位置の直前に移動します。
  • AvoidInstantiatingObjectsInLoops(Avoid instantiating new objects inside loops)は指摘箇所を修正しようと思ったのですが、ループの外でインスタンスを生成するよう修正できなかったので、pmd-project-rulesets.xml<exclude name="AvoidInstantiatingObjectsInLoops"/> を追加して、この rule を外すことにします。

rulesets/java/strings.xml

  • ConsecutiveAppendsShouldReuse(StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.)は、指摘通り StringBuilder#append をチェーンするよう変更します。
  • AppendCharacterWithChar(Avoid appending characters as strings in StringBuffer.append.)は、StringBuilder#append の引数が1文字だけの場合には "a"'a' のようにシングルクォーテーションに変更します。
  • AvoidDuplicateLiterals(The String literal "..." appears ... times in this file; the first occurrence is on line ...)は、フィールド定数を定義して、それを使用するよう変更します。

rulesets/java/unnecessary.xml

  • UnnecessaryModifier(Avoid modifiers which are implied by the context)は、インターフェースに定義したメソッドに public を付けていたので削除します。

rulesets/java/unusedcode.xml

  • UnusedPrivateMethod(Avoid unused private methods such as ...)は、@SuppressWarnings({"PMD.UnusedPrivateMethod"}) を付加してメッセージが出力されないようにします。
  • UnusedLocalVariable(Avoid unused local variables such as ...)は、未使用の変数が必要のないよう処理を変更するか、未使用の変数を削除します。

clean タスク → Rebuild Project → build タスクを実行する

上の修正では /config/pmd/pmd-project-rulesets.xml に記載した ReleSet を一旦全てコメントアウトしてから、1つずつコメントアウトを解除して適用しながら確認していました。最後に全て適用した形で build タスクを実行します。

リンク先の内容 が最終版の /config/pmd/pmd-project-rulesets.xml です。

clean タスク → Rebuild Project → build タスク の順に実行し、"BUILD SUCCESSFUL" が出力されることを確認します。

f:id:ksby:20170708093657p:plain

メモ書き

コンソールに出力されるメッセージがどの RuleSet のどの Rule なのかをどうやって探すのか?

例えば rulesets/java/design.xml だけ適用してチェックしていた時に Consider using varargs for methods or constructors which take an array the last parameter. のメッセージが出力された場合、以下の手順で該当の Rule を探しました。

  1. https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java の下の design.xml をクリックする。
  2. ページ内で Consider using varargs for methods or constructors which take an array the last parameter. の文字列を検索する。message="Consider using varargs for methods or constructors which take an array the last parameter." がヒットして、これが UseVarargs の Rule であることが分かる。
  3. design.xml の UseVarargs の description を読んだり、https://pmd.github.io/pmd-5.8.0/pmd-java/rules/java/design.html の UseVarargs の説明を読んで変更方法を考える。

適用している RuleSet の xml を開いて、出力されているメッセージの文字列で検索する方法になります。複数の RuleSet を適用している場合には、https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java の一番上に表示されている GitHub の検索フィールドに文字列を入力して検索してもいいかもしれません。

ただし、この方法では Rule が見つからない場合があります。

  • メッセージ内のメソッド名や変数名等動的に変わる部分はそのまま検索しても当然ヒットしないので、それ以外の部分で検索すること。
  • メッセージで動的な部分以外で検索しても全然ヒットしない場合があります。例えば naming.xml の VariableNamingConventions(Variables that are final and static should be all capitals, ... is not all capitals.)は “final and static” で検索してもヒットしませんでした。結局 “final” だけで検索してヒットした Rule を1つずつ確認して判断しています。

    メッセージが出力されている Rule の RuleSet が分からず、メッセージで検索してもヒットしない状態だとすごく探しづらいと思います。分からない場合にはまず1つずつ RuleSet を適用していきながら、メッセージが出力されている Rule が含まれる RuleSet を探すところから始めましょう(たぶん)。

RuleSet を複数適用してメッセージが大量に出力されたらどうするか?

一番最初がこの状態でしたが、この場合には RuleSet を1つずつ適用しながら(他の RuleSet はコメントアウトしながら)対応方法を考えるのが良いと思います。

PMD を 5.8.0 → 5.8.1 へ上げて build タスクを実行する

ここまで書いてから PMD のバージョンを確認したところ、5.8.1 がリリースされていました。5.8.1 はバージョンアップしておきます。

build.gradle の以下の点を変更します。

pmd {
    toolVersion = "5.8.1"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • toolVersion = "5.8.0"toolVersion = "5.8.1" に変更します。

clean タスク → Rebuild Project → build タスク の順に実行します。5.8.1 にバージョンアップしても追加で指摘されたところはありませんでした。

f:id:ksby:20170708114354p:plain

※上の実行前に試しに1度実行したので、PMD 5.8.1 のモジュールのダウンロード処理は出力されていません。

最後に

PMD は checkstyleFindBugs とは別の観点から指摘してくれる(一部重複するところはある)ようで、これも入れた方が良いと思いました。ただし全ての RuleSet を無条件に適用するのは無理があるので(不要と思う RuleSet あるいは Rule は必ず出て来ます)、プロジェクトにあったものだけ適用するよう調整が必要です。

ソースコード

pmd-project-rulesets.xml

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>project rulesets</description>

    <!--
        rulesets の種類・説明は 以下の URL 参照
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
        https://pmd.github.io/pmd-5.8.0/pmd-java/rules/index.html
        ※"pmd-5.8.0" の部分は適用しているバージョンに変更すること。
    -->
    <rule ref="rulesets/java/basic.xml"/>
    <rule ref="rulesets/java/braces.xml"/>
    <rule ref="rulesets/java/clone.xml"/>
    <rule ref="rulesets/java/codesize.xml">
        <exclude name="TooManyMethods"/>
    </rule>
    <rule ref="rulesets/java/comments.xml/CommentRequired">
        <properties>
            <property name="fieldCommentRequirement" value="Ignored"/>
            <property name="publicMethodCommentRequirement" value="Ignored"/>
            <property name="protectedMethodCommentRequirement" value="Ignored"/>
            <property name="enumCommentRequirement" value="Ignored"/>
        </properties>
    </rule>
    <rule ref="rulesets/java/design.xml">
        <exclude name="UseUtilityClass"/>
        <exclude name="UncommentedEmptyMethodBody"/>
        <exclude name="UncommentedEmptyConstructor"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>
    <rule ref="rulesets/java/empty.xml"/>
    <rule ref="rulesets/java/finalizers.xml"/>
    <rule ref="rulesets/java/imports.xml"/>
    <rule ref="rulesets/java/logging-java.xml"/>
    <rule ref="rulesets/java/naming.xml">
        <exclude name="ShortVariable"/>
        <exclude name="LongVariable"/>
        <exclude name="ShortMethodName"/>
        <exclude name="VariableNamingConventions"/>
        <exclude name="ShortClassName"/>
    </rule>
    <rule ref="rulesets/java/optimizations.xml">
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="AvoidInstantiatingObjectsInLoops"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="UseStringBufferForStringAppends"/>
        <exclude name="RedundantFieldInitializer"/>
    </rule>
    <rule ref="rulesets/java/strictexception.xml">
        <exclude name="SignatureDeclareThrowsException"/>
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="AvoidCatchingGenericException"/>
    </rule>
    <rule ref="rulesets/java/strings.xml"/>
    <rule ref="rulesets/java/sunsecure.xml"/>
    <rule ref="rulesets/java/typeresolution.xml">
        <exclude name="SignatureDeclareThrowsException"/>
    </rule>
    <rule ref="rulesets/java/unnecessary.xml">
        <exclude name="UselessParentheses"/>
    </rule>
    <rule ref="rulesets/java/unusedcode.xml"/>
</ruleset>

履歴

2017/07/08
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その11 )( build.gradle への PMD の導入 )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その10 )( 起動時の spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する ) の続きです。

参照したサイト・書籍

  1. PMD
    https://pmd.github.io/

  2. Gradle で PMD による静的解析を実行する
    http://maku77.github.io/gradle/pmd/pmd.html

  3. mychaelstyle/build.gradle - Example Gradle build Java with FindBugs and PMD and CPD
    https://gist.github.com/mychaelstyle/9826322

  4. How to find PMD Rulesets names in Gradle >2.0
    https://stackoverflow.com/questions/25584501/how-to-find-pmd-rulesets-names-in-gradle-2-0

  5. pmd/pmd - pmd/pmd-java/src/main/resources/rulesets/java/
    https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java

    • build.gradle の ruleSets に記述する名称はどこを見ればよいのかがよく分からなかったのですが、どうもこのディレクトリを見ればよいらしいです。末尾の “.xml” を取り除いて、先頭に “java-” を付ければ ruleSet 名になります。
  6. How can getters/setters be ignored in the PMD CommentRequired rule?
    https://stackoverflow.com/questions/30869538/how-can-getters-setters-be-ignored-in-the-pmd-commentrequired-rule

  7. How to make a new rule set
    https://pmd.github.io/pmd-5.7.0/customizing/howtomakearuleset.html#

  8. @SuppressWarnings more than one rule not working
    https://stackoverflow.com/questions/22855796/suppresswarnings-more-than-one-rule-not-working

目次

  1. build.gradle を変更する
  2. build タスクを実行する
  3. RuleSets を1つずつ適用して出力されるメッセージを確認し、適用するか否か判断する
    1. java-basic
    2. java-braces
    3. java-clone
    4. java-codesize
    5. java-comments
    6. java-controversial
    7. java-coupling
    8. java-design
    9. java-empty
    10. java-finalizers
    11. java-imports
    12. java-logging-jakarta-commons
    13. java-logging-java
    14. java-naming
    15. java-optimizations
    16. java-strictexception
    17. java-strings
    18. java-sunsecure
    19. java-typeresolution
    20. java-unnecessary
    21. java-unusedcode
    22. pmd-project-rulesets.xml を作成し、build.gradle を変更する
  4. 再び build タスクを実行する
  5. 続きます。。。

手順

build.gradle を変更する

  1. build.gradle を リンク先のその1の内容 に変更します。

  2. 変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

build タスクを実行する

  1. clean タスク → Rebuild Project → build タスク の順に実行します。

    build タスクが実行されるといろいろダウンロードされます。また Removed misconfigured rule: LoosePackageCoupling cause: No packages or classes specified というメッセージも出力されていました。

    f:id:ksby:20170624153320p:plain

    その後 PMD のチェック処理が実行されて 1761 PMD rule violations were found. というメッセージが出力されました。checkstyle, findbugs で警告・エラーが出なくなっているので大丈夫なのでは?と思っていましたが、かなり大量にメッセージが出ますね。。。

    f:id:ksby:20170624154136p:plain

    そんなに大量に指摘されても全て対応することはできないので、RuleSet を1つずつ確認して、どのようなメッセージが出るのか、またその RuleSet を適用すべきか、を判断します。

RuleSet を1つずつ適用して出力されるメッセージを確認し、適用するか否か判断する

build.gradle の ruleSets に指定する RuleSet を1つだけにして clean タスク → Rebuild Project → build タスク の順に実行し、以下の方針で判断します。

  • Web で PMD を設定している build.gradle を調べてみると java-basic, java-braces の2つを適用している例がよく見られたので、この2つは必ず適用することにします。ただしメッセージが出力されるのか否かは確認します。
  • それ以外は出力されるメッセージと PMD Rulesets index: Current Rulesets のマニュアルでチェックされる内容を確認して、適用するか否かを判断します。

また適用の方法については、最終的には build.gradle の ruleSets に指定する方法ではなく、別の xml ファイルで細かく条件を指定する方法に変更します。作成する xml ファイルは /config/pmd/pmd-project-rulesets.xml とし、以下の内容のファイルに RuleSet を追加していきます。

<?xml version="1.0"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>custom rulesets</description>

    .....(ここに RuleSet を記述します).....

</ruleset>

java-basic

java-basic は何もメッセージは出ませんでした。これはそのまま適用します。

    <rule ref="rulesets/java/basic.xml"/>

java-braces

java-braces は何もメッセージは出ませんでした。これはそのまま適用します。

    <rule ref="rulesets/java/braces.xml"/>

java-clone

java-clone は何もメッセージは出ませんでした。

Clone Implementation でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/clone.xml"/>

java-codesize

java-codesize は 2 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • This class has too many methods, consider refactoring it.

Code Size でチェック内容を確認した上で、以下のルールで適用することにします。

  • TooManyMethods(This class has too many methods, consider refactoring it.)は指摘されてもすぐに修正できなさそうなので、外します。
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/codesize.xml">
        <exclude name="TooManyMethods"/>
    </rule>

java-comments

java-comments は 479 PMD rule violations were found. という結果でした。メッセージの種類は8種類で、以下のメッセージでした。

  • headerCommentRequirement Required
  • fieldCommentRequirement Required
  • Comment is too large: Line too long
  • publicMethodCommentRequirement Required
  • protectedMethodCommentRequirement Required
  • To avoid mistakes add a comment at the beginning of the ... field if you want a default access modifier
  • Comment is too large: Too many lines
  • enumCommentRequirement Required

Comments でチェック内容を確認した上で、以下のルールで適用することにします。

  • CommentRequired の headerCommentRequirement(headerCommentRequirement Required)だけ指摘して欲しいと思ったので(checkstyle でクラスのコメントがないのをチェックできていないことに気付いたので)、これだけ適用します。
    <rule ref="rulesets/java/comments.xml/CommentRequired">
        <properties>
            <property name="fieldCommentRequirement" value="Ignored"/>
            <property name="publicMethodCommentRequirement" value="Ignored"/>
            <property name="protectedMethodCommentRequirement" value="Ignored"/>
            <property name="enumCommentRequirement" value="Ignored"/>
        </properties>
    </rule>

java-controversial

java-controversial は 169 PMD rule violations were found. という結果でした。メッセージの種類は12種類で、以下のメッセージでした。

  • Each class should declare at least one constructor
  • Avoid unnecessary constructors - the compiler will generate these for you
  • It is a good practice to call super() in a constructor
  • Use explicit scoping instead of the default package private level
  • Found 'DD'-anomaly for variable ...
  • Found 'DU'-anomaly for variable ...
  • If you run in Java5 or newer and have concurrent access, you should use the ConcurrentHashMap implementation
  • A method should have only one exit point, and that should be the last statement in the method
  • Found 'UR'-anomaly for variable ...
  • Avoid using Literals in Conditional Statements
  • This statement may have some unnecessary parentheses
  • Assigning an Object to null is a code smell. Consider refactoring.

Controversial でチェック内容を確認しましたが、この RuleSet に必要性を感じなかったので、適用しないことにします。

java-coupling

java-coupling は 247 PMD rule violations were found. という結果でした。メッセージの種類は3種類で、以下のメッセージでした。またこの RuleSets を適用すると Removed misconfigured rule: LoosePackageCoupling cause: No packages or classes specified のメッセージも表示されました。

  • Potential violation of Law of Demeter (method chain calls)
  • Potential violation of Law of Demeter (object not created locally)
  • Potential violation of Law of Demeter (static property access)

Coupling でチェック内容を確認しましたが、この RuleSet に必要性を感じなかったので、適用しないことにします。

java-design

java-design は 55 PMD rule violations were found. という結果でした。メッセージの種類は11種類で、以下のメッセージでした。

  • All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.
  • Document empty method body
  • Consider using varargs for methods or constructors which take an array the last parameter.
  • Consider simply returning the value vs storing it in local variable '...'
  • Document empty constructor
  • Private field '...' could be made final; it is only initialized in the declaration or constructor.
  • Avoid autogenerated methods to access private fields and methods of inner / outer classes
  • New exception is thrown in catch block, original stack trace may be lost
  • A class which only has private constructors should be final
  • Class cannot be instantiated and does not provide any static methods or fields
  • Deeply nested if..then statements are hard to read

Design でチェック内容を確認した上で、以下のルールで適用することにします。

  • UseUtilityClassAll methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.)は Application.javaSpring Framework の @Component アノテーションを付加していて Helper クラスにしているものが検知されてしまうので、外します。
  • UncommentedEmptyMethodBodyDocument empty method body)は @Aspect を付加したクラスの @Pointcut アノテーションを付加したメソッドが検知されていたので、外します。
  • MissingStaticMethodInNonInstantiatableClassClass cannot be instantiated and does not provide any static methods or fields)は ValuesHelper クラスが検知されていて修正のしようがないので、外します。
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/design.xml">
        <exclude name="UseUtilityClass"/>
        <exclude name="UncommentedEmptyMethodBody"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>

java-empty

java-empty は何もメッセージは出ませんでした。

Empty Code でチェック内容を確認すると指摘してもらいたい点が多かったので、そのまま適用することにします。

    <rule ref="rulesets/java/empty.xml"/>

java-finalizers

java-finalizers は何もメッセージは出ませんでした。

Finalizer でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/finalizers.xml"/>

java-imports

java-design は 55 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • Unnecessary use of fully qualified name ... due to existing import ...

Import Statements でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/imports.xml"/>

java-logging-jakarta-commons

java-logging-jakarta-commons は 2 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • There is log block not surrounded by if

Jakarta Commons Logging でチェック内容を確認すると指摘して欲しい点が記載されているのですが、試してみると期待通り動作しないようです。

  • There is log block not surrounded by if が出力されたところは確かに logger.info(...) しか記述しておらず if (logger.isInfoEnabled()) { ... } は記述していなかったのですが、他に logger.info(...) しか記述していないところでメッセージが出ていない箇所があります。
  • GuardDebugLogging の rule は検知して欲しいと思ったのですが、logger.debug(...) だけ記述してもメッセージが何も表示されませんでした。

org.slf4j.Logger を使用しているからでしょうか。。。? 動作していないようなので、外すことにします。

java-logging-java

java-logging-java7 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • The Logger variable declaration does not contain the static and final modifiers

Java Logging でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/logging-java.xml"/>

java-naming

java-naming は 175 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • Variables that are final and static should be all capitals, ... is not all capitals.
  • Avoid excessively long variable names like ...
  • Avoid variables with short names like ...
  • Only variables that are final should contain underscores (except for underscores in standard prefix/suffix), ... is not final.
  • The field name indicates a constant but its modifiers do not
  • Variables should start with a lowercase character, ... starts with uppercase character.
  • Avoid short class names like ...

Naming でチェック内容を確認した上で、以下のルールで適用することにします。

  • 以下の rule は必要性を感じなかったので外します。
    • ShortVariable(Avoid variables with short names like ...
    • LongVariable(Avoid excessively long variable names like ...
    • ShortMethodName
    • ShortClassName(Avoid short class names like ...
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/naming.xml">
        <exclude name="ShortVariable"/>
        <exclude name="LongVariable"/>
        <exclude name="ShortMethodName"/>
        <exclude name="ShortClassName"/>
    </rule>

java-optimizations

java-optimizations は 527 PMD rule violations were found. という結果でした。メッセージの種類は6種類で、以下のメッセージでした。

  • Parameter ... is not assigned and could be declared final
  • Local variable ... could be declared final
  • Avoid declaring a variable if it is unreferenced before a possible exit point.
  • Avoid instantiating new objects inside loops
  • Prefer StringBuffer over += for concatenating strings
  • Avoid using redundant field initializer for ...

Optimization でチェック内容を確認した上で、以下のルールで適用することにします。

  • 以下の2つの rule はそこまで final だらけにしたくないと思ったので、外します。
    • MethodArgumentCouldBeFinal(Parameter ... is not assigned and could be declared final
    • LocalVariableCouldBeFinal(Local variable ... could be declared final
  • AvoidInstantiatingObjectsInLoops(Avoid instantiating new objects inside loops)は適用しますが、CSV ファイルのレコードをループでチェックしている処理の中の errors.reject(..., new Object[]{...}, ...) のように指摘を受けても修正しようがない箇所もあったので、その場合には @SuppressWarnings({PMD.AvoidInstantiatingObjectsInLoops}) アノテーションを付加して PMD のチェックが行われないようにします。
  • UseStringBufferForStringAppends(Prefer StringBuffer over += for concatenating strings) は、簡単な文字列結合でも指摘を受けていて、さすがにそこまで全部 StringBuffer で処理したくないので外します。
  • RedundantFieldInitializer(Avoid using redundant field initializer for ...)は入れた方がよいのか否かちょっと悩みましたが、例えデフォルトでも明示的に書きたい場合もあると思ったので、外すことにします。
    <rule ref="rulesets/java/optimizations.xml">
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="UseStringBufferForStringAppends"/>
        <exclude name="RedundantFieldInitializer"/>
    </rule>

java-strictexception

java-strictexception は 14 PMD rule violations were found. という結果でした。メッセージの種類は3種類で、以下のメッセージでした。

  • A method/constructor shouldnt explicitly throw java.lang.Exception
  • Avoid throwing raw exception types.
  • Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block

Strict Exceptions でチェック内容を確認した上で、以下のルールで適用することにします。

  • SignatureDeclareThrowsException(A method/constructor shouldnt explicitly throw java.lang.Exception)は throws Exception としか書きようがないところまで指摘されていたので、外します。
  • AvoidThrowingRawExceptionTypes(Avoid throwing raw exception types.)は RuntimeException ではなくてきちんと場面に応じた例外を throw しようという指摘なのですが、安直に RuntimeException を使いたい時もあるので、外すことにします。
  • AvoidCatchingGenericException(Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block)も Exception を catch したい時があるので外します。
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/strictexception.xml">
        <exclude name="SignatureDeclareThrowsException"/>
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="AvoidCatchingGenericException"/>
    </rule>

java-strings

java-strings は 30 PMD rule violations were found. という結果でした。メッセージの種類は3種類で、以下のメッセージでした。

  • StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.
  • Avoid appending characters as strings in StringBuffer.append.
  • The String literal ... appears ... times in this file; the first occurrence is on line ...

String and StringBuffer でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/strings.xml"/>

java-sunsecure

java-sunsecure は何もメッセージは出ませんでした。

Security Code Guidelines でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/sunsecure.xml"/>

java-typeresolution

java-typeresolution は 6 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • A method/constructor shouldnt explicitly throw java.lang.Exception

Type Resolution でチェック内容を確認した上で、以下のルールで適用することにします。

  • SignatureDeclareThrowsException(A method/constructor shouldnt explicitly throw java.lang.Exception)は削除できない throws Exception を指摘されていたので、外します。
  • それ以外の rule は適用します。
    <rule ref="rulesets/java/typeresolution.xml">
        <exclude name="SignatureDeclareThrowsException"/>
    </rule>

java-unnecessary

java-unnecessary は 41 PMD rule violations were found. という結果でした。メッセージの種類は2種類で、以下のメッセージでした。

  • Avoid modifiers which are implied by the context
  • Useless parentheses.

Unnecessary でチェック内容を確認した上で、 以下のルールで適用することにします。

  • UselessParentheses(Useless parentheses.)は削除できない箇所を指摘されていたので、外します。
  • それ以外の rule は適用します。
    <rule ref="rulesets/java/unnecessary.xml">
        <exclude name="UselessParentheses"/>
    </rule>

java-unusedcode

java-unnecessary は 6 PMD rule violations were found. という結果でした。メッセージの種類は2種類で、以下のメッセージでした。

  • Avoid unused private methods such as ...
  • Avoid unused local variables such as ...

Unused Code でチェック内容を確認した上で、以下のルールで適用することにします。

  • この RuleSet はそのまま適用します。
  • メッセージが出力された箇所で指摘を受ける必要がないところは @SuppressWarnings({...}) アノテーションを付加して PMD のチェックが行われないようにします。
    <rule ref="rulesets/java/unusedcode.xml"/>

pmd-project-rulesets.xml を作成し、build.gradle を変更する

  1. /config/pmd/pmd-project-rulesets.xml を新規作成し、リンク先の内容 を記述します。

  2. build.gradle を リンク先のその2の内容 に変更します。

再び build タスクを実行する

  1. clean タスク → Rebuild Project → build タスク の順に実行します。

    今度は 219 PMD rule violations were found. という結果でした。

    f:id:ksby:20170630053245p:plain

続きます。。。

次回、指摘を受けた箇所を修正します。また今回記事を書き始めた時は PMD のバージョンは 5.7.0 だったのですが、その後で 5.8.0 が出たようです。5.8.0 に変更して確認しながら修正します。

ソースコード

build.gradle

■その1

..........

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'de.undercouch.download'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
apply plugin: 'pmd'

..........

pmd {
    toolVersion = "5.7.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSets = [
            // ruleSet の種類・説明は 以下の URL 参照
            // https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
            // https://pmd.github.io/pmd-5.7.0/pmd-java/rules/index.html
            'java-basic'
            , 'java-braces'
            , 'java-clone'
            , 'java-codesize'
            , 'java-comments'
            , 'java-controversial'
            , 'java-coupling'
            , 'java-design'
            , 'java-empty'
            , 'java-finalizers'
            , 'java-imports'
            , 'java-logging-jakarta-commons'
            , 'java-logging-java'
            , 'java-migrating'
            , 'java-naming'
            , 'java-optimizations'
            , 'java-strictexception'
            , 'java-strings'
            , 'java-sunsecure'
            , 'java-typeresolution'
            , 'java-unnecessary'
            , 'java-unusedcode'
    ]
}
  • apply plugin: 'pmd' を追加します。
  • pmd { ... } を追加します。ruleSets は java-j2ee, java-javabeans, java-junit, java-migrating を除き一旦全部入れています。

■その2

pmd {
    toolVersion = "5.7.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml") を追加します。
  • ruleSets = [] に変更します。

pmd-project-rulesets.xml

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>project rulesets</description>

    <!--
        rulesets の種類・説明は 以下の URL 参照
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
        https://pmd.github.io/pmd-5.7.0/pmd-java/rules/index.html
        ※"pmd-5.7.0" の部分は適用しているバージョンに変更すること。
    -->
    <rule ref="rulesets/java/basic.xml"/>
    <rule ref="rulesets/java/braces.xml"/>
    <rule ref="rulesets/java/clone.xml"/>
    <rule ref="rulesets/java/codesize.xml">
        <exclude name="TooManyMethods"/>
    </rule>
    <rule ref="rulesets/java/comments.xml/CommentRequired">
        <properties>
            <property name="fieldCommentRequirement" value="Ignored"/>
            <property name="publicMethodCommentRequirement" value="Ignored"/>
            <property name="protectedMethodCommentRequirement" value="Ignored"/>
            <property name="enumCommentRequirement" value="Ignored"/>
        </properties>
    </rule>
    <rule ref="rulesets/java/design.xml">
        <exclude name="UseUtilityClass"/>
        <exclude name="UncommentedEmptyMethodBody"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>
    <rule ref="rulesets/java/empty.xml"/>
    <rule ref="rulesets/java/finalizers.xml"/>
    <rule ref="rulesets/java/imports.xml"/>
    <rule ref="rulesets/java/logging-java.xml"/>
    <rule ref="rulesets/java/naming.xml">
        <exclude name="ShortVariable"/>
        <exclude name="LongVariable"/>
        <exclude name="ShortMethodName"/>
        <exclude name="ShortClassName"/>
    </rule>
    <rule ref="rulesets/java/optimizations.xml">
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="UseStringBufferForStringAppends"/>
        <exclude name="RedundantFieldInitializer"/>
    </rule>
    <rule ref="rulesets/java/strictexception.xml">
        <exclude name="SignatureDeclareThrowsException"/>
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="AvoidCatchingGenericException"/>
    </rule>
    <rule ref="rulesets/java/strings.xml"/>
    <rule ref="rulesets/java/sunsecure.xml"/>
    <rule ref="rulesets/java/typeresolution.xml">
        <exclude name="SignatureDeclareThrowsException"/>
    </rule>
    <rule ref="rulesets/java/unnecessary.xml">
        <exclude name="UselessParentheses"/>
    </rule>
    <rule ref="rulesets/java/unusedcode.xml"/>
</ruleset>

履歴

2017/06/30
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その10 )( 起動時の spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その9 )( Spring Boot を 1.5.3 → 1.5.4 にバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • IntelliJ IDEA 2017.2 Public Preview の記事が出ていたので読んでみたのですが、その中の「Replacing multiple equals with Set.contains」を見て、Application クラスで spring.profiles.active をチェックしている処理に適用できそうなので、変更することにします。

参照したサイト・書籍

  1. IntelliJ IDEA 2017.2 Public Preview
    https://blog.jetbrains.com/idea/2017/06/intellij-idea-2017-2-public-preview/

目次

  1. Application クラスの spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する

手順

Application クラスの spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する

src/main/java/ksbysample/webapp/lending/Application.java を以下のように変更します。

public class Application {

    private static final Set<String> springProfiles = Collections
            .unmodifiableSet(new HashSet<>(Arrays.asList("product", "develop", "unittest")));

    /**
     * Spring Boot メインメソッド
     *
     * @param args ???
     */
    public static void main(String[] args) {
        String springProfilesActive = System.getProperty("spring.profiles.active");
        if (!springProfiles.contains(springProfilesActive)) {
            throw new UnsupportedOperationException(
                    MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で "
                                    + "develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。"
                            , springProfilesActive));
        }

        SpringApplication.run(Application.class, args);
    }

}
  • private static final Set<String> springProfiles = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("product", "develop", "unittest"))); を追加します。
  • main メソッド内の if 文で springProfilesActive を StringUtils.equals でチェックしていた処理を !springProfiles.contains(springProfilesActive) に変更します。

動作を確認してみます。bootRun タスクで Tomcat を起動してみると、正常に起動します。

f:id:ksby:20170623061241p:plain

Tomcat を停止した後、build.gradle の bootRun に記述している spring.profiles.active を developdevelopx に変更してから再度 bootRun で Tomcat を起動しようとしてみると、

bootRun {
    jvmArgs = ['-Dspring.profiles.active=developx']
}

今度はエラーになって起動しませんでした。チェックは正常に機能しているようです。

f:id:ksby:20170623061602p:plain

clean タスク → Rebuild Project → build タスクを実行すると “BUILD SUCCESSFUL” のメッセージが出力されることも確認できます。

f:id:ksby:20170623062244p:plain

IntelliJ IDEA 2017.2 は「Spring Boot: dashboard & actuator endpoints」が一番期待している機能なのですが、他にもいろいろ面白そうだったり、よく分からない機能があって、リリースが楽しみです。でも GIFアニメーションがちょっと速すぎて、もう少しゆっくり見たいんだけどな、とも思いました。。。

履歴

2017/06/23
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 番外編 )( Groovy + JUnit4 でテストを書いてみる、Groovy SQL を使ってみる )

概要

記事一覧こちらです。

Groovy でテストを書く場合 Spock を使用していますが、Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 番外編 )( static メソッドをモック化してテストするには? ) で JUnit4 形式でも書けることに気付いたので、Groovy + JUnit4 で書く場合のサンプルを作ってみます。

また Spock をもっと使えるようにしたいと思い、最近以下の本を購入して読んでいるのですが、

Java Testing With Spock

Java Testing With Spock

Spock: Up and Running: Writing Expressive Tests in Java and Groovy

Spock: Up and Running: Writing Expressive Tests in Java and Groovy

Groovy SQL というものがあることを知りました。DbUnit もいいのですが、DB のデータを手軽に確認したい場合には少し手間がかかるので、Groovy SQL を利用して簡単に確認する方法もサンプルに入れてみます。

参照したサイト・書籍

  1. groovy.sql - Class Sql
    http://docs.groovy-lang.org/latest/html/api/groovy/sql/Sql.html

  2. Groovyで楽にSQLを実行してみよう
    https://www.slideshare.net/simosako/db-16699219

    • Groovy SQL の使い方が分かりやすくまとまっていて非常に参考になりました。

目次

  1. build.gradle を修正して groovy-all を依存関係に追加する
  2. Java + JUnit4 ではなく Groovy + JUnit4 でテストを書いた時に便利な点とは
    1. Groovy では .class は不要
    2. テストメソッド名を “” で囲んで自由に書ける
    3. テストメソッドに throws Exception の記述が不要
    4. Spcok でなくてもテストメソッド内で given:, when:, then:setup:, expect: が書ける(単なるコメントとしてだけのようですが)
    5. Spock の時と同様に PowerAssert の分かりやすいレポートが出せる
  3. Groovy + JUnit4 + Groovy SQL のサンプルを書いてみる
  4. Groovy SQL、IntelliJ IDEA + Groovy SQL で書くと便利な点とは
    1. SQL 文に IntelliJ IDEA の SQL の Code Style が適用される
    2. なぜかメソッドの引数の SQL 文に、テーブル名、カラム名等の補完が効く
    3. SQL 文を書く時にヒアドキュメントが使える
  5. 最後に

手順

build.gradle を修正して groovy-all を依存関係に追加する

以前コンパイルした時にエラーが出たため build.gradle で groovy-all を除外していたのですが、このままでは groovy SQL が使えませんでした。使えるようにするために groovy-all を依存関係に追加します。

まず dependencies の testCompile("org.spockframework:spock-core:${spockVersion}"), testCompile("org.spockframework:spock-spring:${spockVersion}") のところに書いていた { exclude module: "groovy-all" } を削除します。

dependencies {

    ..........

    testCompile("org.spockframework:spock-core:${spockVersion}")
    testCompile("org.spockframework:spock-spring:${spockVersion}")

    ..........

}

変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

更新後 clean タスク → Rebuild Project を実行すると以下のエラーが出ます。

f:id:ksby:20170617154842p:plain

以前はこれを見て groovy-all の方を除外してしまったのですが、groovy 方を除外するようにします。コマンドラインから gradlew dependencies を実行して出力された結果を見ると、org.codehaus.groovy:groovy を依存関係に入れているのは org.springframework.boot:spring-boot-starter-thymeleaf でした。

+--- org.springframework.boot:spring-boot-starter-thymeleaf: -> 1.5.4.RELEASE
|    +--- org.springframework.boot:spring-boot-starter:1.5.4.RELEASE (*)
|    +--- org.springframework.boot:spring-boot-starter-web:1.5.4.RELEASE (*)
|    +--- org.thymeleaf:thymeleaf-spring4:2.1.5.RELEASE -> 3.0.6.RELEASE
|    |    +--- org.thymeleaf:thymeleaf:3.0.6.RELEASE
|    |    |    +--- ognl:ognl:3.1.12
|    |    |    |    \--- org.javassist:javassist:3.20.0-GA -> 3.21.0-GA
|    |    |    +--- org.attoparser:attoparser:2.0.4.RELEASE
|    |    |    +--- org.unbescape:unbescape:1.1.4.RELEASE
|    |    |    \--- org.slf4j:slf4j-api:1.6.6 -> 1.7.25
|    |    \--- org.slf4j:slf4j-api:1.6.6 -> 1.7.25
|    \--- nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:1.4.0 -> 2.2.2
|         +--- nz.net.ultraq.thymeleaf:thymeleaf-expression-processor:1.1.3
|         |    +--- org.codehaus.groovy:groovy:2.4.6 -> 2.4.11
|         |    \--- org.thymeleaf:thymeleaf:3.0.0.RELEASE -> 3.0.6.RELEASE (*)
|         +--- org.codehaus.groovy:groovy:2.4.6 -> 2.4.11
|         \--- org.thymeleaf:thymeleaf:3.0.0.RELEASE -> 3.0.6.RELEASE (*)

build.gradle の compile("org.springframework.boot:spring-boot-starter-thymeleaf") の記述を以下のように変更します。

dependencies {

    ..........

    compile("org.springframework.boot:spring-boot-starter-thymeleaf") {
        exclude group: "org.codehaus.groovy", module: "groovy"
    }

    ..........

}

変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新してから、clean タスク → Rebuild Project を実行するとエラーが出なくなります。また build タスクをすると “BUILD SUCCESSFUL” が出力されることも確認できます。

f:id:ksby:20170617160412p:plain

Java + JUnit4 ではなく Groovy + JUnit4 でテストを書いた時に便利な点とは

Groovy で書くのでいろいろシンプルになりますが、特に便利と思った点をメモしておきます。

Groovy では .class は不要

Groovy では .class を書く必要がありません。例えば @RunWith(Enclosed), @RunWith(SpringRunner) のように書けます。

@RunWith(Enclosed)
class SampleTest {

    @RunWith(SpringRunner)
    @SpringBootTest
    static class テストクラス {

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル1"() {

        }

    }

}

テストメソッド名を “” で囲んで自由に書ける

Java + JUnit4 では使えなかった ‘.’, ‘-’ や半角スペース等が自由に記述できます。

        @Test
        void "テストメソッド名に .,- () 等も使えます"() {

        }

テストメソッドに throws Exception の記述が不要

上下のサンプルを見ると分かるように、Java + JUnit4 の時にはテストメソッドに付けていた throws Exception を書く必要がなくなります。

Spcok でなくてもテストメソッド内で given:, when:, then:setup:, expect: が書ける(単なるコメントとしてだけのようですが)

given:, when:, then: の記述は Spock だから出来るものと思っていたのですが、Groovy + JUnit4 の場合でも問題なく書けます。

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル1"() {
            setup: "データを追加する"

            expect: "データが追加されているか確認する"
        }

Spock の時と同様に PowerAssert の分かりやすいレポートが出せる

※実例はこの後に書きます。

Groovy + JUnit4 + Groovy SQL のサンプルを書いてみる

以下のような処理をするテストを2パターン書きます。

  1. TestDataResource クラスが指定されたテーブルのバックアップを取得した後、用意されたデータをロードします。
  2. Groovy SQL でテーブルにデータを追加します。
  3. Groovy SQL でテーブルからデータを取得して検証します。
  4. TestDataResource クラスがバックアップのデータをリストアします。
package ksbysample.webapp.lending

import groovy.sql.Sql
import ksbysample.common.test.rule.db.BaseTestData
import ksbysample.common.test.rule.db.TestDataResource
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner

import javax.sql.DataSource

@RunWith(Enclosed)
class SampleTest {

    // テストクラス内で共通で使用する定数を定義する
    class TestConst {
        static def TEST_ROLE_01 = "ROLE_USER"
        static def TEST_ROLE_02 = "ROLE_ADMIN"
    }

    @RunWith(SpringRunner)
    @SpringBootTest
    // @BaseTestData は TestDataResource クラスが使用する自作のアノテーション
    @BaseTestData("testdata/base")
    static class テストクラス {

        @Autowired
        private DataSource dataSource

        // 自作のテーブルのバックアップ・リストア用クラス
        @Rule
        @Autowired
        public TestDataResource testDataResource

        Sql sql

        @Before
        void setUp() {
            sql = new Sql(dataSource)
        }

        @After
        void tearDown() {
            sql.close()
        }

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル1"() {
            setup: "データを追加する"
            sql.execute("INSERT INTO user_role(role_id, user_id, role) VALUES (?, ?, ?)"
                    , [100, 1, TestConst.TEST_ROLE_01])

            expect: "データが追加されているか確認する"
            def row = sql.firstRow("SELECT * FROM user_role WHERE role_id = 100 AND user_id = 1")
            assert row.role == "ROLE_USER"
        }

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル2"() {
            setup: "データを3件追加する"
            sql.withBatch("INSERT INTO user_role(role_id, user_id, role) VALUES (?, ?, ?)") {
                it.addBatch([100, 6, TestConst.TEST_ROLE_01])
                it.addBatch([101, 7, TestConst.TEST_ROLE_01])
                it.addBatch([102, 7, TestConst.TEST_ROLE_02])
            }

            expect: "追加されたデータをチェックする(カラムは全て取得するが role カラムだけチェックする)"
            def rows = sql.rows("SELECT * FROM user_role WHERE user_id IN (6, 7) ORDER BY role_id")
            assert rows.role == ["ROLE_USER", "ROLE_USER", "ROLE_ADMIN"]

            and: "追加されたデータをチェックする(role_id, role の2カラムのみ取得してチェックする)"
            rows = sql.rows("""\
                SELECT
                  role_id,
                  role
                FROM user_role
                WHERE user_id IN (6, 7)
                ORDER BY role_id
            """)
            assert rows == [[role_id: 100, role: "ROLE_USER"]
                            , [role_id: 101, role: "ROLE_USER"]
                            , [role_id: 102, role: "ROLE_ADMIN"]]
        }

    }

}

テストを実行すると成功します。

f:id:ksby:20170617164346p:plain

ちなみに assert row.role == "ROLE_USER"assert row.role == "ROLE_xxx" に変更してテストを失敗させると、Spock の時と同様に PowerAssert の分かりやすいレポートが出力されます。

f:id:ksby:20170617165219p:plain

Groovy SQLIntelliJ IDEA + Groovy SQL で書くと便利な点とは

SQL 文に IntelliJ IDEA の SQL の Code Style が適用される

上のコードでは分かりませんが、IntelliJ IDEA 上だと下のキャプチャのように SQL 文に IntelliJ IDEA の Code Style が適用されて色付きで表示されます。

f:id:ksby:20170617172901p:plain

適用される Code Style の設定は IntelliJ IDEA の「Settings」ダイアログの中の「Editor」-「Code Style」-「SQL」で設定されているものです。

f:id:ksby:20170617173017p:plain

また Ctrl+Alt+L を押してコードフォーマットすると、一部だけですが適用されます。分かる範囲では、小文字で書いていた SQL 文が大文字に変更されましたが、自動で改行されたり不要なスペースが削除されたりはしませんでした。

例えば以下のように SQL 文を全て小文字で書いていて Ctrl+Alt+L を押すと、

f:id:ksby:20170617173801p:plain

以下のように大文字に変更されます。

f:id:ksby:20170617173910p:plain

なぜかメソッドの引数の SQL 文に、テーブル名、カラム名等の補完が効く

なんでこんなところで SQL 文の補完が有効になるの? と不思議になるくらい補完が効きます。ヒアドキュメントで書いている SQL 文でも有効でした。便利です!

f:id:ksby:20170617174152p:plain f:id:ksby:20170617174315p:plain f:id:ksby:20170617174557p:plain

SQL 文を書く時にヒアドキュメントが使える

Groovy で書いているので、文字列にヒアドキュメントが使えます。"""\ ... """を前後に付ければ改行を入れて SQL 文が記述できます。ただし Ctrl+Alt+L を押してもヒアドキュメントの文字列はフォーマットされないので注意が必要です。

f:id:ksby:20170617175057p:plain

最後に

Spock ではなく JUnit4 で書く場合でも Groovy が便利だし、Groovy SQL も便利すぎます。今後テストは基本 Groovy で書こうと思います。

履歴

2017/06/17
初版発行。
2017/06/21
* テストメソッドに @Test を付け忘れていたり、戻り値の型を void にしていない箇所を修正しました。
* テストメソッド名を "" で囲んで自由に書ける を追加しました。
* テストメソッドに throws Exception の記述が不要 を追加しました。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その9 )( Spring Boot を 1.5.3 → 1.5.4 にバージョンアップする )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その8 )( logback-develop.xml, logback-unittest.xml, logback-product.xml の設定を logback-spring.xml と application.properties に移動してファイルを削除する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Spring Boot の 1.5.4 がリリースされているのでバージョンアップします。
    • Error Prone 以外のライブラリも最新バージョンにバージョンアップします。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. clean タスク → Rebuild Project → build タスクを実行する

手順

build.gradle を変更する

  1. build.gradle を リンク先の内容 に変更します。

  2. 変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    Spring Boot が 1.5.4.RELEASE にバージョンアップされていることを確認します。

    f:id:ksby:20170617082209p:plain

clean タスク → Rebuild Project → build タスクを実行する

  1. clean タスク → Rebuild Project → build タスクを実行します。

    無事 “BUILD SUCCESSFUL” のメッセージが表示されています。

    f:id:ksby:20170617082742p:plain

  2. Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行すると、こちらも全てのテストが成功しました。

    f:id:ksby:20170617083241p:plain

ソースコード

build.gradle

group 'ksbysample'
version '1.5.4-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.5.4.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "https://repo.spring.io/release/" }
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.10")
        // for Grgit
        classpath("org.ajoberstar:grgit:1.9.3")
        // Gradle Download Task
        classpath("de.undercouch:gradle-download-task:3.2.0")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'de.undercouch.download'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'

sourceCompatibility = 1.8
targetCompatibility = 1.8

task wrapper(type: Wrapper) {
    gradleVersion = '3.5'
}

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN']

// for Doma 2
// JavaクラスとSQLファイルの出力先ディレクトリを同じにする
processResources.destinationDir = compileJava.destinationDir
// コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する
compileJava.dependsOn processResources

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

configurations {
    // for Doma 2
    domaGenRuntime
}

checkstyle {
    configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml")
    toolVersion = '7.8.1'
    sourceSets = [project.sourceSets.main]
}

findbugs {
    toolVersion = '3.0.1'
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    effort = "max"
    excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml")
}

tasks.withType(FindBugs) {
    // Gradle 3.3以降 + FindBugs Gradle Plugin を組み合わせると、"The following errors occurred during analysis:"
    // の後に "Cannot open codebase filesystem:..." というメッセージが大量に出力されるので、以下の doFirst { ... }
    // のコードを入れることで出力されないようにする
    doFirst {
        def fc = classes
        if (fc == null) {
            return
        }
        fc.exclude '**/*.properties'
        fc.exclude '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

repositories {
    jcenter()
}

dependencyManagement {
    imports {
        // BOM は https://repo.spring.io/release/io/spring/platform/platform-bom/Brussels-SR3/
        // の下を見ること
        mavenBom("io.spring.platform:platform-bom:Brussels-SR3") {
            bomProperty 'guava.version', '21.0'
            bomProperty 'thymeleaf.version', '3.0.6.RELEASE'
            bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
            bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
            bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
            bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'
        }
    }
}

bootRepackage {
    mainClass = 'ksbysample.webapp.lending.Application'
}

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:42.1.1"
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.16.1"
    def lombokVersion = "1.16.16"
    def errorproneVersion = "2.0.15"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
    compile("org.thymeleaf.extras:thymeleaf-extras-java8time")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-freemarker")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-data-redis")
    compile("org.springframework.boot:spring-boot-starter-amqp")
    compile("org.springframework.boot:spring-boot-devtools")
    compile("org.springframework.session:spring-session")
    compile("org.springframework.retry:spring-retry")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
    compile("com.google.guava:guava")
    compile("org.apache.commons:commons-lang3")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.springframework.security:spring-security-test")
    testCompile("org.yaml:snakeyaml")
    testCompile("org.mockito:mockito-core")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtime("${jdbcDriver}")
    compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
    compile("org.simpleframework:simple-xml:2.7.1")
    compile("com.univocity:univocity-parsers:2.4.1")
    testCompile("org.dbunit:dbunit:2.5.3")
    testCompile("com.icegreen:greenmail:1.5.5")
    testCompile("org.assertj:assertj-core:3.8.0")
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
    testCompile("org.jsoup:jsoup:1.10.3")
    testCompile("cglib:cglib-nodep:3.2.5")
    testCompile("org.spockframework:spock-core:${spockVersion}") {
        exclude module: "groovy-all"
    }
    testCompile("org.spockframework:spock-spring:${spockVersion}") {
        exclude module: "groovy-all"
    }
    testCompile("com.google.code.findbugs:jsr305:3.0.2")

    // for lombok
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
    testCompileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Doma
    compile("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("${jdbcDriver}")

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")
}

..........
  • version '1.5.3-RELEASE'version '1.5.4-RELEASE' へ変更します。
  • buildscript の以下の点を変更します。
    • springBootVersion = '1.5.3.RELEASE'springBootVersion = '1.5.4.RELEASE' へ変更します。
    • maven { url "http://repo.spring.io/repo/" }maven { url "https://repo.spring.io/release/" } へ変更します。
    • classpath("org.ajoberstar:grgit:1.9.2")classpath("org.ajoberstar:grgit:1.9.3") へ変更します。
  • checkstyle の以下の点を変更します。
    • toolVersion = '7.7'toolVersion = '7.8.1' へ変更します。
  • dependencyManagement の以下の点を変更します。
    • mavenBom("io.spring.platform:platform-bom:Brussels-SR2")mavenBom("io.spring.platform:platform-bom:Brussels-SR3") へ変更します。
  • dependencies の以下の点を変更します。
    • def jdbcDriver = "org.postgresql:postgresql:42.0.0"def jdbcDriver = "org.postgresql:postgresql:42.1.1" へ変更します。
    • def domaVersion = "2.16.0"def domaVersion = "2.16.1" へ変更します。
    • testCompile("com.icegreen:greenmail:1.5.4")testCompile("com.icegreen:greenmail:1.5.5") へ変更します。
    • testCompile("org.assertj:assertj-core:3.7.0")testCompile("org.assertj:assertj-core:3.8.0") へ変更します。
    • testCompile("org.jsoup:jsoup:1.10.2")testCompile("org.jsoup:jsoup:1.10.3") へ変更します。

履歴

2017/06/17
初版発行。

IntelliJ IDEA を 2017.1.3 → 2017.1.4 へ、Git for Windows を 2.13.0 → 2.13.1(2) へバージョンアップ

IntelliJ IDEA を 2017.1.3 → 2017.1.4 へバージョンアップする

IntelliJ IDEA の 2017.1.4 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. 「Platform and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170617063441p:plain

  3. Plugin の update も表示されました。「Error-prone Compiler Integration」はバージョンアップすると動かなくなりますので、これだけチェックを外して「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170617063651p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. メイン画面が表示されます。IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170617065544p:plain

  6. 処理が終了すると Gradle Tool Window のツリーの表示が other グループしかない初期の状態に戻っていますので、左上の「Refresh all Gradle projects」ボタンをクリックして更新します。更新が完了すると build グループ等が表示されます。

  7. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.1.4 へバージョンアップされていることを確認します。

  8. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。

    f:id:ksby:20170617070325p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170617070802p:plain

Git for Windows を 2.13.0 → 2.13.1(2) へバージョンアップする

Git for Windows の 2.13.1(2) がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.13.1.2-64-bit.exe をダウンロードします。

  2. Git-2.13.1.2-64-bit.exe を実行します。

  3. 「Git 2.13.1.2 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Configuring the line ending conversions」画面が表示されます。「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git のバージョンが git version 2.13.1.windows.2 になっていることを確認します。

    f:id:ksby:20170617071849p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170617072051p:plain

  13. 特に問題はないようですので、2.13.1(2) で作業を進めたいと思います。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 番外編 )( static メソッドをモック化してテストするには? )

概要

記事一覧はこちらです。

Spring Framework の DI コンテナが管理するクラスのインスタンスのメソッドから DI コンテナで管理していないクラスの static メソッドが呼び出されている場合に、static メソッドをモック化して、Spring Framework の DI コンテナが管理するクラスのテストを書く方法があるのか知りたくなったので、いろいろ調べてみました。今回はそのメモ書きです。

モック化しないテストは Groovy + Spock で書きたいので、モック化する場合のテストも可能なら Groovy + Spock で、Spock が無理なら Groovy + JUnit4 で書く方法を調べています。

参照したサイト・書籍

  1. Spock Framework Reference Documentation - Interaction Based Testing
    http://spockframework.org/spock/docs/1.1/interaction_based_testing.html

  2. PowerMock
    https://github.com/powermock/powermock

  3. PowerMockとJUnitのRuleを使うときのメモ
    http://irof.hateblo.jp/entry/20130517/p1

  4. Mockito + PowerMock LinkageError while mocking system class
    https://stackoverflow.com/questions/16520699/mockito-powermock-linkageerror-while-mocking-system-class

目次

  1. テストで使用するクラスを書く
  2. Spock で正常処理のテストを書く
  3. static メソッドをモック化して実施したいテストとは?
  4. Groovy + Spock + GroovyMock で static メソッドをモック化してテストを書いてみる
  5. Groovy + JUnit4 + PowerMock で static メソッドをモック化してテストを書いてみる
  6. Groovy + Spock + PowerMock で static メソッドをモック化してテストを書いてみる
  7. Spock ではうまく動作しないので JUnit4 でテストを書いてみる
  8. まとめ
  9. PowerMock で void のメソッドをモック化するには?

手順

テストで使用するクラスを書く

今回テストで使用するクラスを2つ書きます。SampleHelper クラスと BrowfishUtils クラスです。

最終的に static メソッドのモックを作成してテストを書きたいのは SampleHelper クラスです。以下の仕様です。

  • SampleHelper クラスは @Component アノテーションを付加して Spring Framework の DIコンテナでインスタンス化します。
  • SampleHelper#encrypt メソッドは BrowfishUtils#encrypt メソッドを呼び出して渡された文字列を Browfish で暗号化します。
  • BrowfishUtils#encrypt メソッドは static メソッドです。
  • BrowfishUtils#encrypt メソッドは処理の中で発生する例外をそのまま throw しますが、SampleHelper#encrypt メソッドは RuntimeException に置き換えます。
  • 余談ですが、今回は static メソッドのテストの方法を調べるのが目的なので BrowfishUtils クラスでは暗号化モードには ECB を使用したのですが、ECB を使うと Error Prone が InsecureCryptoUsage というチェックエラーを出力します(ECB で暗号化した文字列は常に同じになるので暗号化としてふさわしくない、というエラーのようです)。何となく作ったサンプルでしたが、Error Prone はこんな所までチェックするのか、と驚きました。
package ksbysample.webapp.lending;

import org.springframework.stereotype.Component;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

@Component
public class SampleHelper {

    public String encrypt(String str) {
        try {
            return BrowfishUtils.encrypt(str);
        } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
                | BadPaddingException | IllegalBlockSizeException e) {
            throw new RuntimeException(e);
        }
    }

}
package ksbysample.webapp.lending;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class BrowfishUtils {

    private static final String KEY = "sample";
    private static final String ALGORITHM = "Blowfish";
    private static final String TRANSFORMATION = "Blowfish/ECB/PKCS5Padding";

    public static String encrypt(String str)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException
            , BadPaddingException, IllegalBlockSizeException {
        SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        byte[] encryptedBytes = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

}

Spock で正常処理のテストを書く

以下のテストを書きます。

package ksbysample.webapp.lending

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

@SpringBootTest
class SampleHelperTest extends Specification {

    @Autowired
    private SampleHelper sampleHelper

    @Unroll
    def "SampleHelper.encrypt(#str) --> #result"() {
        expect:
        sampleHelper.encrypt(str) == result

        where:
        str        || result
        "test"     || "bjMKKlhqu/c="
        "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw=="
    }

}

実行して成功することを確認します。

f:id:ksby:20170609013513p:plain

static メソッドをモック化して実施したいテストとは?

最終的に書きたいのは以下の内容のテストです。

  • BrowfishUtils#encrypt メソッドをモック化して、呼び出されたら NoSuchPaddingException が throw されるようにします。
  • SampleHelper#encrypt メソッドを呼び出したら RuntimeException が throw されることを確認します。

Groovy + Spock + GroovyMock で static メソッドをモック化してテストを書いてみる

Interaction Based Testing の中の「Mocking Static Methods」を見ると Spock でも static メソッドをモック化できるようです。

最初は SampleHelper#encrypt メソッドではなく BrowfishUtils#encrypt メソッドでテストを書いてみます。

モックを使ったテストは正常処理のテストとは別にしたいので、SampleHelperTest クラスは @RunWith(Enclosed) を付加して extends Specification を削除し、「正常処理のテスト」static class と「異常処理のテスト」static class の2つに分けます。

package ksbysample.webapp.lending

import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

import javax.crypto.NoSuchPaddingException

@RunWith(Enclosed)
class SampleHelperTest {

    @SpringBootTest
    static class 正常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        @Unroll
        def "SampleHelper.encrypt(#str) --> #result"() {
            expect:
            sampleHelper.encrypt(str) == result

            where:
            str        || result
            "test"     || "bjMKKlhqu/c="
            "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw=="
        }

    }

    static class 異常処理のテスト extends Specification {

        def "BrowfishUtils.encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            given:
            GroovyMock(BrowfishUtils, global: true)
            BrowfishUtils.encrypt(_) >> { throw new NoSuchPaddingException() }

            when:
            BrowfishUtils.encrypt("test")

            then:
            thrown(NoSuchPaddingException)
        }

    }

}

テストを実行すると成功しました。

f:id:ksby:20170613005146p:plain

SampleHelper#encrypt メソッドを呼び出すテストに変更してみます。

    @SpringBootTest
    static class GroovyMockを使用した異常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        def "SampleHelper.encryptを呼ぶとRuntimeExceptionをthrowする"() {
            given:
            GroovyMock(BrowfishUtils, global: true)
            BrowfishUtils.encrypt(_) >> { throw new NoSuchPaddingException() }

            when:
            sampleHelper.encrypt("test")

            then:
            thrown(RuntimeException)
        }

    }

テストを実行すると Expected exception of type 'java.lang.RuntimeException', but no exception was thrown というメッセージが出力されて失敗しました。例外を throw するようモックにした BrowfishUtils#encrypt メソッドが SampleHelper#encrypt メソッドからは呼び出されていないようです。

f:id:ksby:20170613005635p:plain

GroovyMock を使う方法だと Spring Framework の DI コンテナが絡んだ時にうまく動作しないようなので、他の方法を探します。

Groovy + JUnit4 + PowerMock で static メソッドをモック化してテストを書いてみる

Spring Boot がサポートしている Mockito で static メソッドをモック化できないか調べてみたところ、Mocito 自体は static メソッドのモック化には対応していませんが、PowerMock を使用するとモック化できるようです。

ライブラリを依存関係に追加します。Mockito_maven のページを参考に build.gradle を以下のように変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

dependencies {
    ..........

    // PowerMock
    testCompile("org.powermock:powermock-module-junit4:1.6.6")
    testCompile("org.powermock:powermock-api-mockito:1.6.6")
}

SampleHelper#encrypt メソッドではなく、BrowfishUtils#encrypt メソッドをモックにして BrowfishUtils#encrypt メソッド自体を呼ぶテストを書いてみます。最初は Groovy + JUnit4 + PowerMock で書きます。

例外が throw されているかチェックするのに AssertJ の assertThatExceptionOfType メソッドを使用したのですが、lambda 式のところで赤波線が表示されてエラーになりました。なぜ?と思って Web で調べてみると、Groovy は lambda 式がサポートされていないことが原因でした。知りませんでした。。。

f:id:ksby:20170609021232p:plain

調べてみたところ、Java の lambda 式は Groovy の Closure でそのまま書けるようです。上のエラーの場合には () -> { ... }{ ... } で書けますので、テストを以下のように書きます。

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(JUnit4)
    @PrepareForTest(BrowfishUtils)
    static class 異常処理のテスト {

        @Test
        void "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(NoSuchPaddingException).isThrownBy({
                BrowfishUtils.encrypt("test")
            })
        }

    }

テストを実行すると成功しました。

f:id:ksby:20170613011138p:plain

Groovy + Spock + PowerMock で static メソッドをモック化してテストを書いてみる

書いたテストを Spock で書き直します。Web で調べると Spock で PowerMock を使いたい場合には PowerMockRule を使うよう書かれている記事を見かけるのですが、PowerMockとJUnitのRuleを使うときのメモ には複数の @Rule が指定されている場合、PowerMockRule は動かなくなる、とも書かれていました。PowerMockRule って使えないのでは?。。。と思いましたが、まずは試してみます。

PowerMockRule を見て build.gradle を以下のように変更します。変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

dependencies {
    ..........

    // PowerMock
    testCompile("org.powermock:powermock-module-junit4:1.6.6")
    testCompile("org.powermock:powermock-api-mockito:1.6.6")
    testCompile("org.powermock:powermock-module-junit4-rule:1.6.6")
    testCompile("org.powermock:powermock-classloading-xstream:1.6.6")
}

まずは JUnit4 のまま PowerMockRule を使う方式へ変えてみます。先程作成した 「異常処理のテスト」static class を以下のように変更します。

    @RunWith(JUnit4)
    @PrepareForTest(BrowfishUtils)
    static class 異常処理のテスト {

        @Rule
        public PowerMockRule powerMock = new PowerMockRule()

        @Test
        void "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(NoSuchPaddingException).isThrownBy(
                    new ThrowableAssert.ThrowingCallable() {
                        @Override
                        void call() throws Throwable {
                            BrowfishUtils.encrypt("test")
                        }
                    })
        }

    }

テストを実行すると成功します。

f:id:ksby:20170613013606p:plain

「異常処理のテスト」static class を Spock で書き直してみます。

    @PrepareForTest(BrowfishUtils)
    static class 異常処理のテスト extends Specification {

        @Rule
        public PowerMockRule powerMock = new PowerMockRule()

        def "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            given:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            when:
            BrowfishUtils.encrypt("test")

            then:
            thrown(NoSuchPaddingException)
        }

    }

テストを実行すると、PowerMockRule 内の処理で NullPointerException が発生して失敗しました。JUnit4 だと動きますが Spock に変えると動きませんね。。。

f:id:ksby:20170613013848p:plain

PowerMockRule ではなく PowerMockRunner を使用する方法に変えてみます。spock.lang.Specification を見ると @RunWith(Sputnik) と記述されていました。Spock の Runner は Sputnik.class のようなので、以下のように書いてみます。

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(Sputnik)
    @PrepareForTest(BrowfishUtils)
    static class 異常処理のテスト extends Specification {

        def "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            given:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            when:
            BrowfishUtils.encrypt("test")

            then:
            thrown(NoSuchPaddingException)
        }

    }

テストを実行すると、成功はしますが Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST というメッセージが出力されています。動きはするようなので、このまま進めます。

f:id:ksby:20170613014306p:plain

今度は @SpringBootTest アノテーションを付けて、SampleHelper#encrypt メソッドのテストを書いてみます。

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(Sputnik)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils)
    static class 異常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        def "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            given:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            when:
            sampleHelper.encrypt("test")

            then:
            thrown(RuntimeException)
        }

    }

テストを実行すると、java.lang.IllegalStateException: Failed to load ApplicationContext のメッセージが出力されて失敗しました。Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.beans.factory.support.BeanNameGenerator]: Specified class is an interface というメッセージも出力されています。

f:id:ksby:20170613014511p:plain

Spock ではうまく動作しないので JUnit4 でテストを書いてみる

以下のようにテストを書き直します。

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(SpringRunner)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils)
    static class 異常処理のテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(RuntimeException).isThrownBy({
                sampleHelper.encrypt("test")
            })
        }

    }

テストを実行すると、こちらも java.lang.IllegalStateException: Failed to load ApplicationContext のメッセージが出力されて失敗しました。ただし、こちらは Caused by: java.lang.LinkageError: loader constraint violation: loader (instance of org/powermock/core/classloader/MockClassLoader) previously initiated loading for a different type with name "javax/management/MBeanServer" というメッセージが出力されます。

f:id:ksby:20170610070939p:plain

stackoverflow を検索してみると Mockito + PowerMock LinkageError while mocking system class の記事が見つかりました。@PowerMockIgnore アノテーションを記述して無視させればよいようです。

テストを以下のように修正します。

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(SpringRunner)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils)
    @PowerMockIgnore("javax.management.*")
    static class 異常処理のテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(RuntimeException).isThrownBy({
                sampleHelper.encrypt("test")
            })
        }

    }

テストを実行すると、何もエラー・警告のメッセージは出力されずに成功しました。

f:id:ksby:20170610071445p:plain

まとめ

  • Groovy + Spock でテストを書く方法は分かりませんでした。。。 PowerMockRule を使用する記事をよく見かけるのですが、なぜかうまく動作しません。
  • Groovy + JUnit4 + PowerMock で動作する方法は分かりました。この方法なら Spock で書いたテストと同じファイル内に書けます。
    • build.gradle の dependencies には以下の2行を記述します。バージョン番号は Mockito_maven のページを見て、その時の最新バージョンにします。
      • testCompile("org.powermock:powermock-module-junit4:1.6.6")
      • testCompile("org.powermock:powermock-api-mockito:1.6.6")
    • static メソッドをモック化するテストを書くクラスに以下のアノテーションを付加します。
      • @RunWith(PowerMockRunner)
      • @PowerMockRunnerDelegate(SpringRunner)
      • @SpringBootTest
      • @PowerMockIgnore("javax.management.*")
    • static メソッドをモック化するテストを書くクラスに、以下のアノテーションでモック化するクラスを記述します。
      • @PrepareForTest(~)
    • テストメソッドの最初に PowerMockito.mockStatic(...) でモック化するクラスを宣言し、PowerMockito.when(...).then~(...) でモック化する static メソッドとモック化した時の挙動を記述します。

テストクラスは最終的に以下のようになりました。

package ksbysample.webapp.lending

import org.junit.Test
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PowerMockIgnore
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
import spock.lang.Specification
import spock.lang.Unroll

import javax.crypto.NoSuchPaddingException

import static org.assertj.core.api.Assertions.assertThatExceptionOfType

@RunWith(Enclosed)
class SampleHelperTest {

    @SpringBootTest
    static class 正常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        @Unroll
        def "SampleHelper.encrypt(#str) --> #result"() {
            expect:
            sampleHelper.encrypt(str) == result

            where:
            str        || result
            "test"     || "bjMKKlhqu/c="
            "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw=="
        }

    }

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(SpringRunner)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils)
    @PowerMockIgnore("javax.management.*")
    static class 異常処理のテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(RuntimeException).isThrownBy({
                sampleHelper.encrypt("test")
            })
        }

    }

}

PowerMock で void のメソッドをモック化するには?

戻り値が void の場合、モック化の書き方が上とは変わります。SampleHelper クラス、BrowfishUtils クラスに以下のように void のメソッドを追加して、

@Component
public class SampleHelper {

    ..........

    public void noReturn(String arg) {
        BrowfishUtils.noReturn(arg);
    }

}
public class BrowfishUtils {

    ..........

    public static void noReturn(String arg) {

    }

}

BrowfishUtils#noReturn が RuntimeException を throw するようモック化してテストを書くと以下のようになります。

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(SpringRunner)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils)
    @PowerMockIgnore("javax.management.*")
    static class voidメソッドをモック化するテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_noReturnを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils)
            PowerMockito.doThrow(new RuntimeException())
                    .when(BrowfishUtils.class, "noReturn", Mockito.any())

            // expect:
            assertThatExceptionOfType(RuntimeException).isThrownBy({
                sampleHelper.encrypt("test")
            })
        }

    }

このテストを実行すると成功します。

f:id:ksby:20170615001407p:plain

履歴

2017/06/15
初版発行。
2017/07/09
* Groovy では “.class” の記述は不要だったので削除しました(例:@RunWith(Enclosed.class)@RunWith(Enclosed))。画像でキャプチャしていたソースは除きます。
* Java の lambda 式は Groovy の Closure で書けることを知ったので、isThrownBy メソッド内の記述を匿名クラスから Groovy の Closure へ修正しました。