【Java】雰囲気でmockしていた自分のためのMockito再入門

Mockitoはテストの際に何度も使ったことがあるが、mockやspy, injectmocks等の用語の意味をなんとなくでしか理解しておらず、使う際に何度も詰まってしまっていた。このたび、公式ドキュメントを改めて読み直してみたのでまとめておく。

javadoc.io

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 201920日目の記事です。

ブログを定期的に書くようになって大体1年くらい経つ。このブログを作ったのは2017年のことだが、最初の頃はほとんど更新せず、2018年は月1回未満のペースだった。しかし2019年は、ほぼ1年にわたって月3-4記事書くことができている。
自分は何かを習慣づけることが決して得意な方ではなく、またブログを最優先事項にしているわけでもない。それでいてなぜブログをここまで続けられているのか自分でも気になっているので、一度振り返ってみたい。自分にとって大きかったのは、Slackコミュニティ・ブログメンタリング・そしてブログに対する意味の捉え直しの3つだったと思う。

Slackコミュニティに入って締め切りを作る

まずブログのことを意識から飛ばさないために、2018年の後半にwrite-blog-every-weekというSlackコミュニティに入った。このAdvent Calendarの他の記事でも紹介されているが、ブログを週1回ペースで書いていき、3週分溜まったら退会というわかりやすいルールのコミュニティだ。
入ってしまった以上、そう簡単に退会にはなりたくない。コミュニティからは水・金・日などに定期的にリマインダーが飛んでくるため、ブログのことをすっかり忘れていたということは起こらなくなった。

ブログメンタリングで習慣づける

しかしコミュニティに入ったばかりの自分は、ブログを自分から書くという習慣が全く根づいていなかった。そのため、コミュニティの力だけでは、締切守りたさにテキトーな記事や技術以外の記事を量産してしまうのではないかという不安が残っていた。
そんなとき、運良くカックさんによるブログメンタリングの募集が回ってきて、2019年の1-3月の間メンタリングをしていただくことになった。

ブログメンタリングは、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-contextkubectl 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 -pgit diff などで差分を見るとき、行単位での追加/削除は表示されるが、行の中のどこが変わったのかは表示してくれない。例えば行の中の一単語を書き換えただけで、しかもその行が長い場合、どこに差分があるのか目で探すのが結構大変だった。
しかし先日、 diff-highlight という便利なモジュールが提供されていることを知り、早速導入してみた。

diff-highlightとは

github.com

gitコマンドの、行単位での差分を探す動作のポストプロセスとして実行され、同じ行の中の差分をハイライトしてくれる。 例えば、行の一部分だけ変えたときの git diff は、今までこんな感じだった。

f:id:Udomomo:20191201171033p:plain

それがこうなる。差分がわかりやすい。

f:id:Udomomo:20191201171052p:plain

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 loggit show コマンドでも diff-highlight が有効化されている。

また、普段 tig を使っている場合、2.2.1以降であれば .tigrc で以下の設定をすると、 tig でも diff-highlight が使えるようになる。

set diff-highlight = true

JJUG CCC 2019 Fallに参加しました

ccc2019fall.java-users.jp

半年に一度のJavaカンファレンスです。個人的にこの半年は扱う技術範囲が広がり、自分の興味や課題意識も大きくなったので、今回は気になるセッションが増えました。
参加したのは以下の3つです。

入門 例外

github.com

例外処理についてのセッションです。今年の6月頃に、 System.exitRuntimeException の違いを問われて答えられなかった苦い思い出があるので、体系的に例外処理を学べる良い機会でした。Javaの例外がオブジェクトとして扱われていることのメリットを初めて実感できた気がします。
時間が足りず、検査例外の話を聞けなかったので、改めて復習するつもりです。

Reliability Engineering Behind The Most Trusted Kafka Platform at LINE

業務でKafkaを触る機会が増えそうなので参加しましたが、Kafka以上に「問題の原因を根本的に解明することの重要性」の方に多くの時間が割かれています。Produce APIのレスポンスタイム遅延の原因を探るため、最終的にサーバの物理レイヤーまで潜っていった話に、そこまでやるのかと終始圧倒されっぱなしでした。
調査に多くの時間を割くことができた理由が気になったのですが、「クライアントから見えるような大きな影響がまだ出ていない状況だったから」とのことでした。その判断をするうえでも、SLOの指標を作り可視化することが大事だと気づきました。

JVMs in Containers: Best Practices

jjug-cfp.cfapps.io

主に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も立ち上がるようになる。

f:id:Udomomo:20191117175710p:plain

また 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を立ててみる。設定ファイルはこちらの記事のものをお借りしつつ、少し変えている。

qiita.com

# 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 で指定してある。
また、 targetPorthttp-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

Apacheサーバの設定を何もしていないので403になってしまうが、リクエストを無事に届けることはできた。