経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

mackerel-plugin-accesslogを中心にMackerelのコードを読んだ


グラフのラベル名をいい感じにする簡単なPRを投げた時のメモ。

[plugin-aws-cloudfront]Replace label name of graph by wafuwafu13 · Pull Request #766 · mackerelio/mackerel-agent-plugins · GitHub


公式ブログを読めばなんとなく機能的な部分はわかった。
mackerel.io
mackerel.io

mackerel-plugin-accesslog/main.gompaccesslog.Do()呼び出されているmp.NewMackerelPlugin(...).Run()は、go-mackerel-plugin/mackerel-plugin.goに定義されている。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

mp.NewMackerelPlugin(&AccesslogPlugin{
	prefix:    *optPrefix,
	file:      flag.Args()[0],
	posFile:   *optPosFile,
	noPosFile: *optNoPosFile,
	parser:    parser,
}).Run()

go-mackerel-plugin/mackerel-plugin.go at 42f2323dad976a17b36639d1ddbefa85ad8e771e · mackerelio/go-mackerel-plugin · GitHub

// Run the plugin
func (mp *MackerelPlugin) Run() {
	if os.Getenv("MACKEREL_AGENT_PLUGIN_META") != "" {
		mp.OutputDefinitions()
	} else {
		mp.OutputValues()
	}
}

このgo-mackerel-pluginmp.OutputValues()mackerel-plugin-accesslogFetchMetricsが呼び出されている。

go-mackerel-plugin/mackerel-plugin.go at 42f2323dad976a17b36639d1ddbefa85ad8e771e · mackerelio/go-mackerel-plugin · GitHub

// OutputValues output the metrics
func (mp *MackerelPlugin) OutputValues() {
	now := time.Now()
	stat, err := mp.FetchMetrics()


mackerel-plugin-accesslogFetchMetricsでは、まずgetReadCloser()p.fileつまり flag.Args()[0]で指定されたアクセスログのファイルが開かれる。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

// FetchMetrics interface for mackerelplugin
func (p *AccesslogPlugin) FetchMetrics() (map[string]float64, error) {
	rc, takeMetrics, err := p.getReadCloser()

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

func (p *AccesslogPlugin) getReadCloser() (io.ReadCloser, bool, error) {
	if p.noPosFile {
		rc, err := os.Open(p.file)
		return rc, true, err
	}

それをbufio.NewReader, Reader.Readlineで読み込んでいくという流れ。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

r := bufio.NewReader(rc)
...
buf, isPrefix, err := r.ReadLine()
...

ちなみにアクセスログのパーサーはoptFormat = flag.String("format", "", "Access Log format ('ltsv' or 'apache')")ltsvapacheが指定されてパースされている。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

switch *optFormat {
case "":
	parser = nil // guess format by log (default)
case "ltsv":
	parser = parsers.LTSV
case "apache":
	parser = parsers.Apache
if p.parser == nil {
	p.parser, l, err = parsers.GuessParser(line)
} else {
	l, err = p.parser.Parse(line)
}

パーサーの実態はGitHub - Songmu/axslogparser: Fairly accurate access Log Parserで、ltsvapacheも指定されていなかった場合は推測してくれるらしい。

あとはret := make(map[string]float64)のマップに結果を集計していってそれを返しておしまい。

mackerel-agent-plugins/accesslog.go at df9c8643da21869d54cf8625dd7523d88b1c4e20 · mackerelio/mackerel-agent-plugins · GitHub

ret[string(fmt.Sprintf("%d", l.Status)[0])+"xx_count"]++
ret["total_count"]++
...
ret[v+"_percentage"] = ret[v+"_count"] * 100 / ret["total_count"]

はじめてOSSに新機能追加した


owという、Lots of built-in validationsOSSに、BigIntのバリデーションを追加した。

github.com
github.com

owに関しては2月頃に3回、型整備のPRを出してマージされていたので、コードには少し馴染みがあった。
https://wafuwafu13.hateblo.jp/#sindresorhusow

OSSの型整備より一歩踏み込んだことがしたいと思い、issueを消化することを目標に最近やっていた。
実際、null, undefinedに起因するバグがあったのでそれを消化できた。

Fix `ow.object` to return `ArgumentError` when `null` or `undefined` is passed by wafuwafu13 · Pull Request #211 · sindresorhus/ow · GitHub

そして今回、BigIntのサポートをするという、約3年前のissueがあったので拾ってやってみた。

Support BigInt validator · Issue #54 · sindresorhus/ow · GitHub

owの構造と、何をどう追加したのかを雑にメモしておく。

まず、README.mdにも載っている以下のコードでArgumentErrorがどのようにして起こるのかをみていく。

import ow from 'ow';

const unicorn = input => {
	ow(input, ow.string.minLength(5));

	// …
};

unicorn('yo');
//=> ArgumentError: Expected string `input` to have a minimum length of `5`, got `yo`


source/predicate/string.tsに、ow.stringに関するコードが定義されており、minLengthの定義は以下のようになっている。

ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

minLength(length: number): this {
    return this.addValidator({
	message: (value, label) => `Expected ${label} to have a minimum length of \`${length}\`, got \`${value}\``,
	validator: value => value.length >= length,
	negatedMessage: (value, label) => `Expected ${label} to have a maximum length of \`${length - 1}\`, got \`${value}\``
    });
}

このvalue => value.length >= lengthのバリデーターは、source/predicates/predicate.tsで発火される。
messageの発火も同じ部分である。

ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

...
result = validator(value);
...
const errorMessage = message(value, label_, result);

このvalidatorの実行結果がtrueでなければ、最終的に独自定義のArgumentErrorが投げられる。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

throw new ArgumentError(message, main, errors);

ArgumentErrorが投げられるのはこれ以外にも、ow(input, ow.type.foo)においてinputtypeの型が合っていない場合がある。
例えばtest/string.tsでテストされているような場合がある。

ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

t.throws(() => {
	ow(12 as any, ow.string);
}, 'Expected argument to be of type `string` but received type `number`');

このバリデーションは、Predicateクラスのコンストラクタで定義されている。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

this.addValidator({
	message: (value, label) => {
		// We do not include type in this label as we do for other messages, because it would be redundant.
		const label_ = label?.slice(this.type.length + 1);

		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
		return `Expected ${label_ || 'argument'} to be of type \`${this.type}\` but received type \`${is(value)}\``;
	},
	validator: value => (is as any)[typeString](value)
});


ここのバリデーションで使われているisは、owと同じ作者が作ったライブラリである。
github.com

今回やったissueに立ち返ってみると、isBigInt detectionがサポートされたので、それをowにも取り込もう、というものだった。
Support BigInt detection · Issue #39 · sindresorhus/is · GitHub

最初は、source/predicates/number.tsbigintメソッドを足して、ow(BigInt(9007199254740991), ow.number.bigint)で判定させようと思ったが、numberbigintで型が違うため、コンストラクタのバリデーションで意図せず弾かれてしまった。
新たにBigIntPredicateクラスを定義し、意図してコンストラクタのバリデーションで弾かれる方針で実装を進めた。

NodeのBigIntサポートが10.4.0からなのでテストが思うように動かなかったりいろいろしたが、無事マージされた。
このOSSの作者はFull-Time Open-Sourcererでレスポンスもレビューも手早くて時差があるものの開発体験がとても良かった。
ソースコードの核となる部分の7割くらいを理解できれば、issueのバグを潰したり新機能開発ができるという肌感がある。

技術書積読消化購読メモ

4月5月
読んだ

  1. Docker Kubernetes実践コンテナ開発入門(Kubernetesから)
  2. Amazon Web Services基礎からのネットワーク&サーバー構築
  3. Kubernetes on AWS
  4. TCP/IPネットワーク入門
  5. Real World HTTP
  6. 入門監視
  7. Mackerelサーバ監視入門
  8. DNSがよくわかる教科書
  9. マイクロサービスアーキテクチャ
  10. オブジェクト指向でなぜ作るのか
  11. ドメイン駆動設計入門
  12. SREサイトリライアビリティエンジニアリング
  13. サイトリライアビリティワークブック

6月中
手動かす

  1. Webブラウザセキュリティ
  2. Go言語でつくるインタプリタ
  3. セキュリティコンテストチャレンジブック

読む

  1. Infrastructure as Code
  2. nginx実践入門
  3. 大規模サービス技術入門
  4. Go言語による並行処理
  5. コンピュータシステムの理論と実装
  6. 情報処理概論
  7. Linuxのしくみ
  8. SQLアンチパターン
  9. Elasticsearch実践ガイド
  10. Webフロントエンドハイパフォーマンスチューニング
  11. DMM.comを支えるデータ駆動戦略
  12. 関数型プログラミングの基礎 JavaScriptを使って学ぶ

7月以降
買って手動かす

  1. GraphQLどれか
  2. 実践 パケット解析 第3版
  3. 現代暗号への招待
  4. 言語論どれか
  5. Goならわかるシステムプログラミング
  6. Rust本どれか
  7. ガベージコレクション 自動的メモリ管理を構成する理論と実装
  8. 関数プログラミング実践入門
  9. 型システム入門 プログラミング言語と型の理論

8月までマストでやらないといけないっぽい
理論系はインターンの波が去ったらでいいか

  1. UdemyのSwift
  2. UdemyのSwift UI
  3. iOSアプリ設計パターン入門
  4. iOSテスト全書
  5. Rust本どれか
  6. WebAssemblyのチュートリアルどれか
  7. GraphQLどれか

axiosの内部構造を理解してadapterの仕組みを解明する


axiosのadapterを使う機会があり、仕組みが気になったのでメモしておく。
github.com

adapterで検索をかけると、lib/core/dispatchRequest.jsの51行目の記述が目についた。
axios/dispatchRequest.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub

var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
...

dispatchRequest.jsは名前からも、lib/core/Axios.jsで使われていることからも、実際にリクエストを送る部分であろう。

まずはdefaults.adapterがなんなのかを調べると、lib/defaults.jsに以下の記述があった。
axios/defaults.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/defaults.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}
...
var defaults = {
    adapter: getDefaultAdapter(),
    ....

typeof XMLHttpRequestchromeのコンソールで調べると以下のようになったので、adapter = require('./adapters/xhr');の元を見にいった。

typeof XMLHttpRequest
"function"
typeof XMLHttpRequest = "function"

lib/adapters/xhr.jsでは、XMLHttpRequestを使ってリクエストが送られていた。

axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub

var request = new XMLHttpRequest();
...
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
...
// Send the request
request.send(requestData);

つまり、axiosをadapterオプションを指定せずに使うと、内部的にはXMLHttpRequestを使った通信が行われ、adapterオプションを指定すると、通信そのものをラップできることが分かる。

オプションの反映はlib/core/mergeConfig.jsを使って行われていることが分かったが、深追いはせずテストコードを眺めるだけに留めた。
axios/mergeConfig.spec.js at main · axios/axios · GitHub

実際にadapterオプションを書いて使ってみた。
この場合リクエストは発生せず、実装したPromiseが返っている。

const axios = require("axios");

const sampleAdapter = () => {
  return new Promise((resolve, reject) => {
    resolve({ data: "sample data" });
  });
};

axios({
  url: "https://example.com/",
  adapter: sampleAdapter,
}).then((res) => console.log(res.data));
$ node index.js
sample data

リクエストを送る部分はlib/core配下にあるものだと思っていたので、adapterオプションを指定しない場合にlib/adapters配下のコードが使われているのが意外だった。

追記:
axiosにPR投げてみたらlib/adapters配下のコードが使われているという理解はあっていた。
github.com

AWA株式会社でのインターンでインフラに強くなりました

株式会社サイバーエージェント全職種向け 実践就業型インターン【オンライン参加可】に3月2日~3月31日の1ヶ月間参加し、インフラに強くなりました(過去の自分比)。

参加するまで

サイバーエージェントは魅力的な会社ですし、人生で1回は行っておきたいという気持ちがまずありました。
また、大規模サービスに触れる機会はもともとあったのですが、インフラがほぼブラックボックスの状態で開発をしていたので、インフラに詳しくなりたいという目的がありました。
個人ではJS/TSを重点的にやることが多く、また、インフラの実務経験がほぼない状態でインフラを志望するのは勇気が要りましたが、春は挑戦の季節ということで応募してみました。
とは言っても、エントリー締め切りがインターンシップ開始月の3ヶ月前の月末だったので、11月あたりにエントリーした記憶があります。
1次面接(人事)でABEMAかAWAに行きたいと伝え、2次面接(エンジニア)でインフラをやりたいと伝えたところ、希望が通ってAWAでインフラをさせてもらうことになりました。
面接とはいっても雑談のような感じで、エントリーから参加するまでは長かったですが、合否の結果は早かったのでストレスなく暮らせました。
(EC2とECSという単語が出てくると少し混乱するというようなレベル感だったので、ゴリゴリの技術面接ではなくてよかったです。)
一応オフラインを希望したのですが、フルリモートでの勤務となりました。
出社できなかったのは残念ですが、メンターさんや人事の方とのコミュニケーションに苦労はしませんでした。
働いてみて、AWAは少数精鋭という印象を受けました。
僕が所属したサーバーチームは自分を含めて4名で、Androidをやりながらサーバーもやるといったようなマルチな方の集団といった感じでした。

やったこと

ここからの内容は以下のスライドにも載せてあります。

speakerdeck.com

  • Terrafromのバージョンアップ(v0.12 -> v0.13)

まずはレポジトリ全体の構成をしっかり把握してから始めました。
ただCIを回してバージョンをあげればいいというわけではなく、運用上、HCLの記述と実際の設定に差分が生じるので、それを解消する必要がありました。

  • 本番LOUNGEサーバーのリソース増強(Terraform/Datadog)

ECSの設定をいじって、タスクが切り替わる様子やDatadogのグラフが遷移する様子をみました。

  • LOUNGE検索の追加(MongoDB/Mongo Connector/Elasticsearch/Jenkins/Ansible)

登場してくるツールやレポジトリが多くなったので、自分がしたことがどこにどう反映されるのかを理解しながら進めることを意識しました。
初めての概念が多かったのでドキュメントをしっかり読みました。
github.com

  • ドキュメントの作成(DocBase)

検索データの同期周りは、1年前のドキュメントしかなかったので、新たに作成しました。
ドキュメントとかテストとかしっかりしたいタイプなのでこういう仕事もよかったです。

  • LOUNGEの検索処理

gRPCのproto定義から始めてGoを書いていきました。
クリーンアーキテクチャが採用されているレポジトリに項目を足す作業だったので、難しい部分はありましたが、見通しは立てやすかったです。
これでつまったりしていました。
github.com

感想

  • 夕会すごい

毎日30分、金曜1時間の夕会に参加させていただきました。
エンジニアの方だけではなく、40名くらいの方々が集まり、念密に施策のすり合わせをしていらっしゃいました。
AWAは毎日聴いているものの、今回はインフラでの参加だったので、開発者としてサービスに対する理解が薄くなるのではないかという心配をしていましたが、異なる職種の方が何をしているのかまで知れました。
また、メンターさんにも毎日1on1で30分以上時間をいただき、質問の対応や進捗の管理をしていただきました。

  • Go読みやすい

普段はGoでの開発はしていなかったので不安だったのですが、インターフェイスから辿っていったり、VSCodeの機能を使ったりして初見でも自力で読める部分は多かったです。
Goでの運用を知れたのもよかったです。

  • インフラ周り楽しい

普段インフラはあまり触っておらず、何をしているか分からなくて怖い印象を持っていたのですが、1つの作業でアプリケーションを大きく動かすことができることに特に楽しみを感じました。

  • 触るレポジトリ多かった

マイクロサービスで構成されているため、自分が関連するところだけでもTerraformのレポジトリを合わせると8つありました。
普段はいわゆるモノレポしか基本触らないので、最初は頭が混乱しましたが、慣れるとやりやすさを感じました。

  • サービスの歴史の理解が重要

どのサービスにも少なからずクセのようなものがあり、なんでこのようなコードになっているのか、どうしてこのツールを用いているのか、に対する理解が働く上で重要だと感じました。

  • 意外と自走できる部分はあった

Goもインフラもほぼ未経験の状態で飛び込んだので、1ヶ月で自分ができることはかなり少ないと思っていましたが、ひとつひとつキャッチアップしていくことでなんとか1エンジニアとして働くことができました。
gomockを使ったりしながらテストまで一貫して書けたり、コード書いてる途中で自分が生んだN+1問題に気づけて修正できたりしたのもよかったです。
どこでもインフラのタスクに手をあげていくぞというやっていきや、独学でももっとインフラの知識を深めていきたいという感情が芽生えたのも大きな成功体験でした。

AWAはこれからも使い続けるサービスですし、今回得た経験もこれから活き続けるので、とても濃い1ヶ月でした。
課題としては、iOS/Android の知識がもっとあれば、得るものもより多かったと思います。
どんな質問にも答えてくださったメンターさん、進捗や目標を管理してくださった人事の方、インターンという場に関わる方々に感謝です、ありがとうございました!

DefinitelyTyped で Ace の型を大幅に改善した


前のこの記事では、Definitely Typedから型をインストールして適用する際に、実際にレポジトリまで見に行かないといけなかった、ということを書いた。

wafuwafu13.hatenadiary.com

今回は、PRを出してマージされたので、そのことを書く。

github.com

PRタイトルにある通り、この型は全て ajaxorg/ace/ace.d.ts から取ってきた。
コミットログを見ると、ace.d.tsファーストコミットが2018年3月DefinitelyTyped/types/ace/index.d.tsファーストコミットが2017年3月 となっており、DefinitelyTypedの方が先に作成されているが、以下のコメントに示す通り、

[ace] Improve by referring to ace.d.ts by wafuwafu13 · Pull Request #51346 · DefinitelyTyped/DefinitelyTyped · GitHub

ace.d.tsの型の方が正確であり、以下の差分に示す通り、

[ace] Improve by referring to ace.d.ts by wafuwafu13 · Pull Request #51346 · DefinitelyTyped/DefinitelyTyped · GitHub

DefinitelyTypedの型にはanyが多く、その役割をきちんと果たしてはいなかった。
DefinitelyTypedのレポジトリは巨大で、cloneするにも検索するにも時間がかかって大変だったが、PRを出した。
ace.d.tsを参考にしてるとはいえ、変更がかなり多いので、レビュワーが大変だなーと思っていたが、2週間経ってもレビュワーは反応しなかった。
botが反応し、違う人にレビュー依頼を出してからは、すぐにマージされてリリースされた。
[ace] Improve by referring to ace.d.ts by wafuwafu13 · Pull Request #51346 · DefinitelyTyped/DefinitelyTyped · GitHub

anyだらけで長い間放置されていたことも、なかなかレビューがされなかったことも、違う人にレビューが回ると一瞬でマージされたことにも、DefinitelyTypedに対する今までの信頼性(主観)が少し損なわれたが、改善できたのはよかった。

参考にしていた、ace.d.tsにも、まだ足りない型定義があったので少し追加しておいた。
github.com

こちらの反応は早かった。

Jest のモック関数を整理する


npqという、npmやyarnでインストールする際に安全確認をするライブラリがあり、そこで使われているaxiosnode-fetchで代替するというissueがあったのでやってみた。

github.com
github.com

置き換えるだけの簡単な作業かと思っていたが、テストを通すのが難しかった。
jest.mockmockImplementationjest.spyOnに対する理解が曖昧だったので、整理したいと思う。
コードはこのレポジトリにまとめた。
github.com


まずは、単純な関数のモックから整理していきたい。
下のように、第2引数とランダムな値を足した値を返す関数があるとする。

function random(a, b, c) {
  return b + Math.random();
}

module.exports = random;

この関数のテストでは、便宜上、ランダムな値は無視したい。
なぜなら、無視しないと返り値が予測できず、テストが通らないからだ。

const random = require("./random");

it("random function", () => {
  expect(random(1, 2, 3)).toBe(2);
  // Expected: 2
  // Received: 2.119340184508286
});

モックする1つの方法として、jest.fn を用い、randomに新しい、未使用の mock functionを代入してしまう方法がある。
しかし、この方法では、後続のテストが壊れてしまう恐れがある。

let random = require("./random");

random = jest.fn(() => 2)

it("random function", () => {
  expect(random(1, 2, 3)).toBe(2);
  // pass
  expect(random(1, 3, 5)).toBe(3);
  // Expected: 3
  // Received: 2
});

そこで、randomモジュールをモックし、random関数が第2引数を返すようにモックすれば、うまくいく。

const random = require("./random");

jest.mock('./random')
random.mockImplementation((a, b, c) => b);

it("random function", () => {
  expect(random(1, 2, 3)).toBe(2);
  // pass
  expect(random(1, 3, 5)).toBe(3);
  // pass
});

これをnode-fetchに対するモックでも応用してみる。
下のように、Promiseを返す関数があるとする。

const fetch = require("node-fetch");

function getByFetch() {
  const data = fetch("http://example.com/");
  return data;
}

module.exports = getByFetch;

これに対するテストは、node-fetchモジュールをモックし、fetch関数がfooを返すようにモックすれば、うまくいく。

const fetch = require("node-fetch");
const getByFetch = require("./fetch");

jest.mock("node-fetch");
fetch.mockImplementation(() => {
  return "foo";
});

it("fetch", () => {
  const data = getByFetch();
  expect(data).toBe("foo");
  // pass
});

次に、オブジェクト内の関数のモックについて整理していく。
このようなオブジェクトがあるとする。

const obj = {
  random: function () {
    return Math.random();
  },
  randomPlusOne: function () {
    return Math.random() + 1;
  },
   foo: function () {
    return "foo";
  },
};

module.exports = obj;

これに対するテストでモックする1つの方法としては、objモジュールごとモックしてしまう方法がある。

const obj = require("./object");

jest.mock("./object", () => {
  return {
    random: () => 1,
    randomPlusOne: () => 2,
    foo: () => 'foo',
  };
});

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  // pass
});

これは、次のようにも書ける。

const obj = require("./object");

jest.mock("./object");
obj.random.mockImplementation(() => 1);
obj.randomPlusOne.mockImplementation(() => 2);
obj.foo.mockImplementation(() => 'foo');

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  // pass
});

しかしこれらは、objモジュールごとモックしているため、例えばfooをモックし忘れたら、存在していないことになってしまう。

const obj = require("./object");

jest.mock("./object");
obj.random.mockImplementation(() => 1);
obj.randomPlusOne.mockImplementation(() => 2);

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  //  Expected: "foo"
  //  Received: undefined
});

jest.spyOnを用いれば、その心配はなくなる。

const obj = require("./object");

jest.spyOn(obj, 'random').mockImplementation(() => 1);
jest.spyOn(obj, 'randomPlusOne').mockImplementation(() => 2);

it("random object", () => {
  expect(obj.random()).toBe(1);
  // pass
  expect(obj.randomPlusOne()).toBe(2);
  // pass
  expect(obj.foo()).toBe('foo')
  // pass
});

これをaxiosに対するモックでも応用してみる。
下のように、Promiseを返す関数があるとする。

const axios = require("axios");

function getByAxios() {
  const data = axios.get("http://example.com/");
  return data;
}

module.exports = getByAxios;

これに対するテストは、以下のように書けばうまくいく。

const axios = require("axios");
const getByAxios = require("./axios");

jest.spyOn(axios, 'get').mockImplementation(() => Promise.resolve('foo'));

it("axios", async () => {
  const data = await getByAxios();
  expect(data).toBe("foo");
});

理解が少し難しいjest.mock/mockImplementation/jest.spyOn あたりを整理した。