【Java】雰囲気でmockしていた自分のためのMockito再入門
Mockitoはテストの際に何度も使ったことがあるが、mockやspy, injectmocks等の用語の意味をなんとなくでしか理解しておらず、使う際に何度も詰まってしまっていた。このたび、公式ドキュメントを改めて読み直してみたのでまとめておく。
mockとは
Mockitoでは、インターフェースやクラスをmockすると、そこに定義されたメソッドを呼んでもコンパイルエラーにならなくなる。また、mockは各メソッドが呼ばれた回数も記憶する。
ドキュメントには、例えとしてListインターフェースをmockしたコードが載っている。(もちろんこんなことをやる機会はほとんどないだろう)
import static org.mockito.Mockito.*; List mockedList = mock(List.class); mockedList.add("one"); mockedList.clear(); verify(mockedList).add("one"); verify(mockedList).clear();
mockの意味はこれだけ。自分が勘違いしていたところでもあるが、mockだけをしてどこにもstubしない場合もある。例えば、単に verify
を使ってメソッドの呼び出し回数をテストしたいだけの時などは、stubする意味がない。
また、上記でmockオブジェクトを定義するところの記述は、 @Mock
アノテーションを使えばより簡潔に書ける。ただしこの場合、テストクラス内(またはその親クラス)のどこかに MockitoAnnotations.initMocks(testClass);
の記載が必要となる。
stubとは
単にmockしただけだと、mockオブジェクトのメソッドは返り値の型に応じてデフォルトの値を返すことしかしない(int型なら0, boolean型ならfalseなど)。これではテストにならない。
そこで、特定の引数によって特定のメソッドが呼び出されたとき、その結果をoverrideすることができる。これがstubである。ナイフで結果を差し込むイメージだろうか。
LinkedList mockedList = mock(LinkedList.class); when(mockedList.get(0)).thenReturn("first"); when(mockedList.get(1)).thenThrow(new RuntimeException());
上の例では、 when
を使っている部分がstubにあたる。
spyとは
Mockitoでは、ある特定のオブジェクトのspyを作ることができる。(mockと違い、対象はオブジェクトである)
spyの特徴は、メソッドを呼び出したときに、spyしたオブジェクト内で定義された本物の処理が呼ばれることにある。先程書いたように、普通のmockではデフォルトの値が返ってくるだけ。
さらに、spyにstubすることもできる。このように一部のメソッドだけstubすると、他のメソッドは本当に呼ばれるので、partial mockingということができる。なお、partial mockingはテストコードを複雑にするとして、使用が推奨されていない。
List list = new LinkedList(); List spy = spy(list); when(spy.size()).thenReturn(100); spy.add("one"); spy.add("two"); System.out.println(spy.get(0)); System.out.println(spy.size());
上記の例では、 spy.add
メソッドは本当に呼び出され、 "one" と "two" の2つの要素が追加される。しかし、 spy.size
メソッドはstubされているため、sizeを取得すると100が返ってくる。
InjectMocks
@InjectMocks
を指定したクラスに、mockオブジェクトやspyオブジェクトをinjectしてくれる。しかし、ドキュメントを読んでみると、どのような場合でもinjectできるわけではないようだ。
injectが成功するのは、以下の3つの場合に限られる。(spyを使う機会は少ないので、以下はmockの場合のみ書いている)
- Constructor Injection: mockしたクラスが、InjectMocks指定されたクラスのコンストラクタ引数になっている
- Property Setter Injection: コンストラクタに引数はないが、mockしたクラスをセットできるsetterがinjectMocks指定されたクラスに定義されている
- Field Injection: コンストラクタに引数はないが、mockしたクラスをセットできるフィールドがinjectmocks指定されたクラスに定義されている
すなわち、mockしたクラスではない別の引数がコンストラクタにある場合、mockしたクラスを扱うsetterやfieldがあっても、injectに失敗する。
約1年間ブログを続けられている理由を振り返る
この記事は、write-blog-every-week Advent Calendar 2019の20日目の記事です。
ブログを定期的に書くようになって大体1年くらい経つ。このブログを作ったのは2017年のことだが、最初の頃はほとんど更新せず、2018年は月1回未満のペースだった。しかし2019年は、ほぼ1年にわたって月3-4記事書くことができている。
自分は何かを習慣づけることが決して得意な方ではなく、またブログを最優先事項にしているわけでもない。それでいてなぜブログをここまで続けられているのか自分でも気になっているので、一度振り返ってみたい。自分にとって大きかったのは、Slackコミュニティ・ブログメンタリング・そしてブログに対する意味の捉え直しの3つだったと思う。
Slackコミュニティに入って締め切りを作る
まずブログのことを意識から飛ばさないために、2018年の後半にwrite-blog-every-weekというSlackコミュニティに入った。このAdvent Calendarの他の記事でも紹介されているが、ブログを週1回ペースで書いていき、3週分溜まったら退会というわかりやすいルールのコミュニティだ。
入ってしまった以上、そう簡単に退会にはなりたくない。コミュニティからは水・金・日などに定期的にリマインダーが飛んでくるため、ブログのことをすっかり忘れていたということは起こらなくなった。
ブログメンタリングで習慣づける
しかしコミュニティに入ったばかりの自分は、ブログを自分から書くという習慣が全く根づいていなかった。そのため、コミュニティの力だけでは、締切守りたさにテキトーな記事や技術以外の記事を量産してしまうのではないかという不安が残っていた。
そんなとき、運良くカックさんによるブログメンタリングの募集が回ってきて、2019年の1-3月の間メンタリングをしていただくことになった。
メリークリスマス!新規ブログメンティを募集しまーす!今回の募集人数は1-3名です.メンタリング開始日は 1/1 (火) です.
— カック@ブロガー / k9u (@kakakakakku) 2018年12月16日
・ネタをどう探すか?
・どう習慣化するか?
・どう読まれる記事を書くか?
・などなど
を話しながらブログ以外のアウトプットにも挑戦してもらおうと思っています.
ブログメンタリングは、write-for-every-week同様に毎週定期的に記事を書くのだが、カックさんからのFBを受けたり、ネタの相談をしたりできる。カックさん自身も毎週ブログを更新されており、特にネタの出し方や記事への膨らませ方のパターンはとても参考になった。このメンタリングを通じて、毎週記事を書くペース感をつかめたと思う。
(ちなみにカックさんのブログメンタリングの取り組みは今も続いている。定期的に募集があるので、興味がある方はぜひ参加してほしい)
ブログの意味を捉え直す
メンタリングが終わった後も、ここからが本番ということで頑張って記事を書いていた。しかし、やはり仕事やプライベートが忙しい時期もあり、必ずしもブログに時間を割ける週ばかりではない。時には過去にストックした小ネタや豆知識でやり過ごしたり、3週分記事が溜まってしまい、コミュニティから退会処理される前に慌てて平日に書いたりする週もあった。
普段の自分ならそこで更新ペースが落ちていったと思う。しかし運が良かったのは、ある日カックさんの過去の登壇資料をもう一度見返したことだった。そこに写っていたこのスライドを見たとき、自分の意識が変わった。
この内容自体はブログメンタリング中にも見ており、自分が3月に書いたメンタリング卒業記事の中にも書いてあった。 ただ、「忙しさのあまりブログが書けなくなりつつあった + 自分のスキルに危機感を持ち始めた」という2つの問題意識を抱えている時に目にしたことで、改めて心に刺さったのだと思う。
確かに、同じように仕事をしていても、「今週は何も成長してないな...」と思う週もあれば「今週はいろんなことを学べた気がする!」と思う週もある。そして自分が多くの学びや気づきを得た週は、そこから自然とネタが出てくるので、後はそれを文章にするだけで記事を更新できていた。
このスライドをきっかけに、自分の中で
- 「今週は忙しくてブログのネタが浮かばなかった」-> 「今週の記事を書けないくらい、ここ最近は何も学んでいなかった」
- 「ブログを続けられるようがんばろう」 -> 「もっと自分から知識や知見を吸収しに行こう。ブログはその記録」
というように意味を捉え直すことができた。
ブログとの付き合い方
そういうわけで、今の自分は記事執筆自体に力を入れている感覚はない。ブログはあくまで学んだことの記録にすぎないからだ。
そのかわり、分からないことや十分な知識がないと感じることがあったら、放置せずにちゃんと調べて、それをメモに残すことを心がけている。メモに残しておけば、時間が経って週末になっても記事を書きやすい。(もちろんその日のうちに記事を書くのがベストなのだが、平日忙しいことに変わりはなく、執筆の時間を取るのは未だに難しい。平日更新を続けている人たちは改めて凄いと思う)
来年もブログは続けるつもりでいるが、特にKPI(PV数やブクマ数など)は置いていない。「write-blog-every-weekを退会にならない」というのみだ。けれど、自分が学んだことが一人でも誰かの役に立ったり、強い人がコメントを書いてくれてさらなる気づきを得られたりすればいいなと思っている。
minikube Clusterをdeleteしたらkubectlが使えなくなった
ローカル環境でKubernetesのClusterを動かす方法はいろいろある。よく紹介されているのはminikubeを使う方法だが、自分の場合Docker Desktopを使っている。Docker DesktopにはKubernetesが付属しており、追加のインストールなしでClusterを立ててkubectlで操作できる。 先日試しにminikubeを少しだけ使ってみたものの、仕事のタスクを抱えていたため使い慣れたDocker Desktopに戻すことにした。
minikubeのClusterを停止させ、削除する。
minikube stop
minikube delete
すると、Docker Desktopをkubectlで操作することができなくなってしまった。
kubectl config use-context development Error from server (NotFound): the server could not find the requested resource
kubectl config current-context
や kubectl config get-contexts
等も試したが同じエラー。
途方にくれていたが、minikubeのClusterを消したせいで、contextを正常に読み込めていないからではないかと気づく。本来であればkubectlはminikubeと一緒にインストールされるのだが、今回はもともとDocker Desktopによってインストールされたkubectlを使っており、そこにminikubeの設定が追加された形になっていそうだった。
そこでminikubeのcontextを削除してみる。
kubectl config delete-context minikube kubectl config use-context development
今度は正しくcontextを切り替えることができ、他の操作もできるようになった。
KubernetesのRBACを触って学ぶ
先日Kubernetesの特定のnamespaceでJobを実行しようとしたところ、権限がなかったため、まずClusterの管理者に権限を申請するところから始める必要があった。これを機会に、KubernetesのRBACについて学んでみた。
RBACとは
RBACとは、Role-based access controlの略。どのリソースに対するどんな操作を許可するかを、UserやServiceAccount単位で指定することができる。
他のリソース同様、RBACもyamlファイルで管理する。Role
の設定ファイルでRoleを定義し、 RoleBinding
の設定ファイルでそのRoleをどのUser, ServiceAccountに付与するかを指定する。
この他に ClusterRole
, ClusterRoleBinding
の設定ファイルも作ることができる。 Role
, RoleBinding
は特定のnamespaceにおける権限を指定するのに対し、 ClusterRole
, ClusterRoleBinding
はnamespaceによらない権限を指定するためにある。名前の通り、Cluster全体に適用される権限ということなのだろう。
新しいRoleを追加する
今回、既存のRoleファイルはnamespace内で全ての操作ができる事実上のmaster権限のものしかなかった。さすがにそこまでの権限は不要なので、新しくRoleファイルを作ることにした。
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: development name: running-job rules: - apiGroups: [""] resources: ["pods","pods/log"] verbs: ["get", "list", "watch"] - apiGroups: ["batch"] resources: ["jobs"] verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
metadata.namespace
には、そのRoleの対象となるnamespaceを指定する。 metadata.name
はこのRoleの名前であり、これはRoleをbindする際にも使う。
権限の内容は rules
に書いていく。apiGroupsは複数指定することができ、それぞれにおいて対象となるresourceと、許可する操作( verbs
)を指定できる。上記のようにするとJobの実行権限の他に、podsの一覧の取得や、各podのログの確認も行うことができる。
なお、公式ドキュメントによると、Roleファイルは"purely additive"であるとのこと。すなわち、AWSのpolicyのように Deny
の設定をすることはできない。
Roleを割り当てる
権限の割り当てはRoleBindingファイルで行う。
kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: running-job-binding namespace: development subjects: - kind: User name: udomomo apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: running-job apiGroup: rbac.authorization.k8s.io
重要なのは、 roleRef.name
の名前をRoleファイルの metadata.name
と一致させること。ここが間違っていると正しくbindすることができない。
Userの部分は、各Kubernetes Clusterのユーザ管理のやり方に従って指定する。今回の場合EKSを使っていたので、AWS IAMアカウントをそのまま指定するだけで良かった。EKSでは権限管理をpolicyではなく独自の方法で行っていると聞いたことがあるが、RBACがまさにその役割を果たしているということがようやく理解できた。
gitのdiff-highlightを使い始めた
git log -p
や git diff
などで差分を見るとき、行単位での追加/削除は表示されるが、行の中のどこが変わったのかは表示してくれない。例えば行の中の一単語を書き換えただけで、しかもその行が長い場合、どこに差分があるのか目で探すのが結構大変だった。
しかし先日、 diff-highlight
という便利なモジュールが提供されていることを知り、早速導入してみた。
diff-highlightとは
git
コマンドの、行単位での差分を探す動作のポストプロセスとして実行され、同じ行の中の差分をハイライトしてくれる。
例えば、行の一部分だけ変えたときの git diff
は、今までこんな感じだった。
それがこうなる。差分がわかりやすい。
diff-highlightの設定
この機能は git
コマンドに同梱されているため、インストールは不要。設定作業のみで使える。
まず .gitconfig
に以下を追記する。
[pager] log = diff-highlight | less show = diff-highlight | less diff = diff-highlight | less
ただしこの設定を有効にするには、 diff-highlight
自体にPATHを通す必要がある。
Macのhomebrewで git
をインストールした場合、バージョンを更新すると diff-highlight
のディレクトリも変わってしまう。その場合、 git-core
のシンボリックリンクに対して /usr/local/bin
からさらにシンボリックリンクを貼るという方法で回避できる。
sudo ln -s /usr/local/share/git-core/contrib/diff-highlight/diff-highlight /usr/local/bin/diff-highlight
この方法は以下の記事で知った。 qiita.com
これで diff-highlight
が使えるようになる。 .gitconfig
に設定を加えたことで、 git log
や git show
コマンドでも diff-highlight
が有効化されている。
また、普段 tig
を使っている場合、2.2.1以降であれば .tigrc
で以下の設定をすると、 tig
でも diff-highlight
が使えるようになる。
set diff-highlight = true
JJUG CCC 2019 Fallに参加しました
半年に一度のJavaカンファレンスです。個人的にこの半年は扱う技術範囲が広がり、自分の興味や課題意識も大きくなったので、今回は気になるセッションが増えました。
参加したのは以下の3つです。
入門 例外
例外処理についてのセッションです。今年の6月頃に、 System.exit
と RuntimeException
の違いを問われて答えられなかった苦い思い出があるので、体系的に例外処理を学べる良い機会でした。Javaの例外がオブジェクトとして扱われていることのメリットを初めて実感できた気がします。
時間が足りず、検査例外の話を聞けなかったので、改めて復習するつもりです。
Reliability Engineering Behind The Most Trusted Kafka Platform at LINE
業務でKafkaを触る機会が増えそうなので参加しましたが、Kafka以上に「問題の原因を根本的に解明することの重要性」の方に多くの時間が割かれています。Produce APIのレスポンスタイム遅延の原因を探るため、最終的にサーバの物理レイヤーまで潜っていった話に、そこまでやるのかと終始圧倒されっぱなしでした。
調査に多くの時間を割くことができた理由が気になったのですが、「クライアントから見えるような大きな影響がまだ出ていない状況だったから」とのことでした。その判断をするうえでも、SLOの指標を作り可視化することが大事だと気づきました。
JVMs in Containers: Best Practices
主にJavaアプリケーションをDockerで動かすときのimageサイズの話でした。Alpine程度であれば使ったことがありましたが、jlinkを使って必要最小限のライブラリしか入っていない実行環境を作る方法などは初めて知りました。また、JDK自体も新しいバージョンになるほどサイズダウンやコンテナ内実行の考慮がなされているため、バージョンを上げるだけでもサイズを小さくできるそうです。
このセッションはデモが多く、実際にDockerfileや出来上がったコンテナのサイズを確認できたことで、サイズダウンの効果を実感できました。
今回は春と比べて、超初心者向けから上級者向けまでセッションが幅広くなったように思います。自分もJavaの中で全然理解できていない分野がいくつかあり、基礎から学び直した方がいいと思っていたところだったので、とても良い機会になりました。
今回は他にも気になるセッション(Maven・DIコンテナ・ロギングなど)がいろいろあったのですが、時間がかぶっているものが多く参加できませんでした。今日の熱気が冷めないうちに資料を読んでおきたいです。
KubernetesのServiceを触って学ぶ
仕事でKubernetesを扱うことが不可欠になりつつあるので、最近重点的に学んでいる。Podの立て方は理解したが、Serviceというものが何をするのかわからなかったので、実際に動かして学んでみる。
今回使っているのは、Docker DesktopのKubernetes。Docker DesktopにはKubernetesが付属されており、 Preference
内の Kubernetes
から enable Kubernetes
を選べばよい。Docker Desktopが起動する際に、KubernetesのClusterも立ち上がるようになる。
また kubectl
コマンドも同梱されており、 初期状態では docker-desktop
というローカルのClusterを指している。
kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE * docker-desktop docker-desktop docker-desktop
Serviceの役割
Serviceとは、PodやReplicaSetに対するサービスディスカバリを提供である。KubernetesのPodはそれぞれ独自のIPアドレスを持つが、Podは一度死んだら復活せず、新しく別のPodが作られIPアドレスも変わる。その場合でも、Pod間での接続を担保するためにServiceがある。
Serviceは、Podの論理的集合に対してCluserIPという仮想のIPアドレスを割り当てる。そのIPアドレスにアクセスすることで、リクエストがkube-proxyを通じて各Podに到達できるようになる。仮にPodが死んで新しくなってもClusterIPは変わらないので、相手となるPodの状態を気にせずにリクエストを送ることができる。
Serviceを作る
まずはPodとServiceを立ててみる。設定ファイルはこちらの記事のものをお借りしつつ、少し変えている。
# php-apache-pod.yaml apiVersion: v1 kind: Pod metadata: name: sample-web-pod labels: app: http-app spec: containers: - name: web-container image: php:7.0-apache ports: - name: http-port containerPort: 80 volumeMounts: - name: documentroot mountPath: /var/www/html volumes: - name: documentroot hostPath: path: /home/username/containers/web/html
#php-apache-service.yaml kind: Service apiVersion: v1 metadata: name: http-service labels: app: http-app spec: selector: app: http-app ports: - name: "service-port" protocol: "TCP" port: 8080 targetPort: http-port type: ClusterIP
Serviceの設定ファイル内では、 selector
の部分でPodのLabelを指定している。 app: http-app
というラベルは、Podの設定ファイルの metadata.labels
で指定してある。
また、 targetPort
は http-port
となっているが、これもPodの設定ファイルの中で定義した名前である。
これを使ってオブジェクトを作り、状態を見てみる。
kubectl create -f php-apache-pod.yaml pod/sample-web-pod created kubectl create -f php-apache-service.yaml service/http-service created
kubectl get pod --show-labels NAME READY STATUS RESTARTS AGE LABELS sample-web-pod 1/1 Running 0 3h34m app=http-app kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE http-service ClusterIP 10.99.212.152 <none> 8080/TCP 6s kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5h41m
http-service
が作られ、ClusterIPが 10.99.212.152
になっている。Cluster内部ではこのIPアドレスを使うことでPodにアクセスできる。
それを確認するために、さらに詳しく調べてみる。
kubectl describe service http-service kubectl describe svc http-service Name: http-service Namespace: default Labels: app=http-app Annotations: <none> Selector: app=http-app Type: ClusterIP IP: 10.99.212.152 Port: service-port 8080/TCP TargetPort: http-port/TCP Endpoints: 10.1.0.5:80 Session Affinity: None Events: <none> ubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES sample-web-pod 1/1 Running 0 5h49m 10.1.0.5 docker-desktop <none> <none>
Serviceの Endpoints
が、PodのIPアドレスに等しいことがわかる。
これをふまえて、Podの中にいるApacheサーバにアクセスしてみる。ClusterIPはClusterの中からしか使えないのでどうするか迷ったが、調べるとこのClusterの中に使い捨てのPodを立ててリクエストを送るやり方が一番楽そうだった。
kubectl run testpod --image=centos:6 --restart=Never -i --rm -- curl -s http://10.99.212.152:8080/ <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>403 Forbidden</title> </head><body> <h1>Forbidden</h1> <p>You don't have permission to access / on this server.<br /> </p> <hr> <address>Apache/2.4.25 (Debian) Server at 10.99.212.152 Port 8080</address> </body></html> pod "testpod" deleted