日々常々

ふつうのプログラマがあたりまえにしたいこと。

privateメソッドをテストしたい

と思うのは、とてもいいこと。

前置き

もし行いたいテストが外的振る舞いを示すものであれば(少なくともテストにより観測できる見通しがなければ「テストしたい」とは思わないだろうから、何かしら外から観測可能なものではある可能性は高い)、それがprivateに閉じていていいものではないと言う気づきのきっかけになる。

と言うのは教科書的回答だけど、外には見せたくないけれど複雑なロジックを包含していて、入念かつ局所的にテストしたいと思うこともある。 この動機はすごく自然。きっとそこはテストしなかったらバグってるし。テストしてもバグが見つからないと言うのもよくあるんだけど。 この手のがどうあるのがいいのかはチーム体制も含めたプロダクトによると思っている。

綺麗な考え方は、独立したコンポーネントとして関心ごとや複雑性を閉じ込め、テストしたいと思った内容にもっと高い格を与える。「格」なんて表現は他で使ったことないけど、privateよりもpublicのが格上、メソッドよりもクラスのが格上、みたいな感じです。伝わるよね? 少なくともメソッドと言う形での識別はできていて、そこをテストしたいと思うくらいは他よりも関心があるんだから。

privateメソッドをテストする方法はいくらでもある。プロダクトをテストのために少しなら歪めてもいいって考え方もある。レガシーコード改善ガイドだったかな。

(22:08追記) レガシーコード改善ガイドで合ってた。「10.1 隠れたメソッド」です。ここでは「private->publicに変更」で、私がよくやるのはパッケージプライベートへの変更。この可視性を緩めるのを「歪める」と表現しています。一番変更量少ないし、パッケージプライベートならpublicにするより影響範囲少ないし。「レガシーコード改善ガイド」の中ではpublicにすることへの抵抗感についても言及されています。気になる方はぜひお読みください。

Javaだとプロダクトを歪めずにテストしようとするとリフレクションが使えるけど、将来のリファクタリングを妨げるようになるのでイマイチおすすめはできない。でもやってもいい場面、やるべき場面もある。ほんとチーム体制も含めたプロダクト次第なんだ。言語によっては歪めずに可能だし。

ここまでが前置きで、正直privateメソッドをテストするかどうかなんてどうでもよかったりするんです。どうするべきかとかどう考えるべきかとかどうやったらできるかとかはそれはそれで楽しいんだけど、そこの話をするつもりはここではないんだ。あ、現場でならやります。

本題

表出した事象を叩き潰してはいけない。ここでは「privateメソッドをテストしたい」と感じたことが表出した事象。事象に直接アプローチする対象療法はこう言う文脈では害悪であり、私は人の感覚はだいたい正しいと思っている。でもなきゃコードスメル(嗅覚)なんて大真面目で言えない。

にもかかわらず、経験の多寡に関わらず「privateメソッドをテストしてはいけない」と言う言葉をまにうけて、「privateメソッドのテストをしたいと思った自分が間違っている」と考えてしまったりする人も結構いる。とても勿体ない。

無理に抑えつけず、テストしたくなったんだから、テストしよう。……ってのは私が「不安をテストに」に軸足を置いて強振できるから言えることだけど、そうでなかったとしても「テストしたい」と思った感覚をもう少し信じてみて欲しい。

信じて向き合って考えたら、本当に要らないかもしれない。でも要らないと言うには何かしらの裏付けが必要になる。先に挙げたように設計で「privateメソッドのテスト」ではなくせるかもしれないし、テスト技法かもしれないし、他の何かかもしれない。それが技術であれば身につけられるから、考えることは無駄にはならない。どうにもならなかったら無理矢理テストしたらいいんじゃないかなとも思う。それがあまりよくなかったら、そのフィードバックを受けることになるはず。(手が離れてしまってフィードバックを受けれないかもしれないけど、それは仕方ない。)

「privateメソッドをテストしてはいけない」って言葉は「privateメソッド だから テストしてはいけない」という呪いに容易に変わってしまう。そうじゃないって言いたい。「privateメソッドをテストしたい」と思った感覚は、その瞬間その場その人にとって、絶対に正しい。「そのprivateメソッドをそのままテストしなきゃいけない」かどうかは別の話。

プログラマなんてものを生業にしててなんだけど、もっと感覚を信じてもいいんじゃないかなって思うんです。

蛇足

privateメソッドのテストについてこのブログに何か書いてないかなーと検索してみたけど、9年前に書いたブログくらい。

irof.hateblo.jp

ここでも 開発の初期段階はともかく最終的にはpublicな属性を相手にしたテストで賄ってしまって削除しちゃったほうがいいと思います。 とか書いてる。まーそうだよねって。結局のところprivateメソッドをprivateなままテストするのは(少なくとJavaにおいては)リファクタリングの妨げになるので、よほどの理由があるときにそのマイナスをプロダクトとして許容して行うものだと思う。

まっとうな話

ここまで思ったことをだーっと「私はこう考えてる」ってのを書こうと思って書きました。なんとなく思い立ったんで。

まっとうな考え方とかを知りたい方のためにリンク置いておきます。

t-wada.hatenablog.jp

開発時に実際にProfileをどう指定するか

導入

SpringBootでProfileを使用するようになると、何かのprofileを指定しないと起動しなくなるような構成になりがちだと思います。 たとえば application-local.ymlapplication-dev.ymlapplication-prod.yml など環境ごとのプロファイルを用意する場合です。

開発効率を考えればProfile無しでも動作するようにしておくのが良いこともあるのですが、それだと設定誤りなどで本番環境で動作してしまうリスクが残ります。 たとえば無印(not default)ではH2で動作するようにしておいて、prod で実際のデータベースに接続するように上書きしていた場合。設定漏れや誤りで上書きできなくても、一見正常に動作し、実はH2だった……などが考えられます。 この問題を検知できる別の仕組みがあれば無印を使用するのも良いかもしれませんが、必ず何かしらのProfileを使用すると言う落とし所が現実かなと思っています。

Profile設定方法

SpringBootでProfileを指定する方法はいくつかありますが、基本的にプロパティと同じ方法が取れます。つまり、次のいずれかを使用すると思います。

ほかにもありますが、よく使うのはこの辺りかなと思います。

詳しくは 公式リファレンス を参照してください。使ったことがある物もありますが、使ったことのない物もたくさんあります。

開発時のProfile指定

開発時のProfile設定方法も上記のいずれかを使用することになりますが、案外厄介です。 と言うのも、MavenやGradle自体のJavaシステムプロパティやコマンドライン引数に渡しても、ビルドツールに設定されます。

ビルドツール自体のプロセスとSpringBootのプロセスが異なるため、Javaシステムプロパティやコマンドライン引数はそのまま渡りません。 渡すためにそれぞれ固有の知識が必要になってきます。たとえば -P で受けたのをテストのプロセスに引き渡すような。 知っていれば迷わないですが、知らなければそのための調べたり試行錯誤したりすることになります。

結局私がどうしているか

環境変数一択になっています。コンテナ運用だと環境変数になりますし、本番の設定方法と同じにしたいって理由もありますが、実行方法にとわず設定方法が揺れないのが理由です。

Gradleだと。

SPRING_PROFILES_ACTIVE=hoge gradle bootRun

Mavenだと。

SPRING_PROFILES_ACTIVE=hoge mvn spring-boot:run

これらはシェルですが、WindowsPowerShellでも $env:SPRING_PROFILES_ACTIVE=hoge でできました。

IDEからの実行でも環境変数に追加でいけます。以下はIDEAの例。Profile複数使い分ける場合とか、それぞれで保存しとくと楽。

f:id:irof:20210215110907p:plain

ここまでは SPRING_PROFILES_ACTIVE と書いてきましたが、画像のように spring.profiles.active でもいけます。 もちろんVM OptionやProgram argumentに追加しても構いませんが、環境変数に統一と言う方針なのでこちらを使用。 AWS SDKを使用する場合はここに AWS_PROFILE を並べたりします。

これ別にProfile専用ではなく、全部のプロパティがこれでいけるんですよね。Profile用の専用の仕組みだとこれは使えない。 SPRING_APPLICATION_JSON 環境変数使えば一括での設定もできるのもたまに便利。たまに。

固定のProfileを設定するのであれば、それぞれのツールに設定してもいいと思います。ローカルで動作させるならどうせ local を指定するでしょうし。そう言うのなら「毎回必ず指定する」のは無駄なので、build.gradleだのpom.xmlだのに固定で書いておく方がいいと思う。

結論

……なんてものはないんだけど。 やり方はいくらでもあるし、どんなやり方をしてもいいと思います。

私が「個人ブログ」とかに求めるのってこういう「他の人はどうやってるか知らんし、もっと効率いい方法あるだろけど、自分はこんなことやったりするよ」な内容なんだよなーと思いながら書いたやつ。

https://twitter.com/irof/status/1357153181873020930

このブログもそう。

関連

irof.hateblo.jp

正常な状態を保つのは大変

正常な状態を保ち続けるのは大変だと言うことを、保てるようになると簡単に忘れちゃう。

正常な状態?

  • デプロイが成功する。
  • テストが通る。
  • コンパイルが通る。
  • ERRORWARN のログが出ない。
  • ビルドがすぐ終わる。
  • すべての変更がバージョン管理されている。

あげるてるとキリがないけど、ここで言う「正常な状態」がどんなものかは伝わると思う。 何を正常と言うかはその場その時で変わるけれど、少なくともコンパイルできない状態を正常とは言わない。

崩すのは簡単

正常な状態は簡単に崩せる。コンパイルエラーにするためには適当な文字を1文字足すか消すかするだけ。でも誰も崩したくて崩さない。

崩れてしまうことってある

変更途中は崩れるもの。仕方ない。

正常な状態を保つのはコストがかかるし、そこに気を取られると遅くなってしまう。スピードが大事。仕方ない。

なりふり構わずにゴールを目指してる間は崩れててもいい。仕方ない。

……ほんとに仕方ないのかなって。

正常な状態は保てない?

スポーツ選手とかエキスパートのフォームって綺麗だと思うんですよ。 ホームラン打った瞬間のバッターの写真とか、マラソンランナーのフォームとか。

全力でやると姿勢が崩れると言うのは素人なんじゃないかなと思ったりする。

私の感覚

デプロイはいつも成功してて欲しい。環境構築時とかは結構落ちるけれど、安定飛行をはじめたら滅多に落ちない。たまに失敗した時は大事件で大慌てするし、次から起こらないようにテストを作ったりする。

テストが通るのは当たり前。なんだけど、私は結構テストはカジュアルに落とす。TDDerだってのもあるけど、「とりあえずテスト実行してみよう!」とか言ったりして、そんなノリで実行するから当然のように落ちる。むしろテストは落ちてこそ、落ちた時が仕事した時とか思ってたりする。テスト駆動開発を継続するで話した内容……2013年ですね。

かと言って「テストが落ちたままにする」は違くて。テストが落ちたらすぐに直す。正常な状態にする。 テストが落ちる崩れた状態で、他のことをしてさらに崩れたら、正常に戻ったと言えなくなっちゃうから。

コンパイルが通るのは流石に当たり前だよね。 と言いたいんだけれど、コンパイルが通ってない瞬間ってのは案外ある。瞬間を切り取れば。 コンパイルが通っているのが当たり前の人には意外かもしれないけれど、コンパイルエラーが発生した状態でそれを解消せずに他のコードを書き続ける人もいたりする。

冒頭に列挙した「正常な状態」が崩れているのは、姿勢が崩れた状態で走り続けてるように感じる。とても落ち着かない。 プロスポーツ選手の例をみれば、フォームを整えた方がいい成績に結びついている(はず)。 綺麗なフォームだとうまく走れなくて遅くなると言うのは、練習不足とか筋力不足とか、フォームが身に付いていない状態なんだろうなと。

実際のとこ

「正常な状態」が開発速度や精度を向上させていることは、崩された時に初めて感じる。 そう言う時に自分が正常と思っているものがそうでもないって世界があることに気づく。

他の人から見たら私も「それくらいできてないの?」と言われるようなこともあると確信してる。 「正常な状態」が崩れているように見えていても、崩れている側はそもそも「正常な状態」を認識できていないとかもある。

できる状態が普通になると、意識して取り組んでいるうちは大変なことを忘れてしまう。 できていない人には大変なのに、できる人には簡単。このギャップが厄介だなぁと思ったり。

どうしたものかなぁって、もやもや。 まとまらないや。