こまぶろ

技術のこととか仕事のこととか。

Java 9 以降の非推奨モジュールに依存したプログラムを動作させる:OpenAPI Generatorで生成したSpringスタブサーバが動作しない場合の対処法

前回の記事で、OpenAPI GeneratorでSpringのスタブサーバのソースコードを生成する方法を紹介した。

ky-yk-d.hatenablog.com

上の記事に記載した手順を手元のMacBook Proで実行したとき、XmlModelPluginに関わるエラーが発生してSpring Bootアプリケーションとして動作しなかった。別の端末(iMac)では正常に動作したので前回の記事では言及しなかったが、調査してみたところわかったことが多かったので、記事として公開することにした。

あらかじめ書いておくが、今回のエラーの根本原因は後述の通り自分がJavaのバージョンに無頓着であったことであり、OpenAPI Generatorの問題ではない。下記の内容は、OpenAPI Generator固有の問題を扱っているものではなく、Javaのバージョンの差異によって生じるエラーのケーススタディ的なものだと思ってもらえばいい。

概要

項目 内容
事象 生成したSpringのスタブサーバが特定環境で動作しない
原因 Java 11 で非推奨のモジュールが解決されなかったため
対応 pom.xmldependenciesJAXB APIを追加する

発生した事象

OpenAPI Genaratorでソースコードを生成したSpringのスタブサーバが動作しなかった。具体的には、下記のような手順でスタブサーバを起動しようとすると、エラーが発生してしまっていた。

実行手順

下記のコマンドを実行し、OpenAPI Generatorによるスタブサーバのソースコードを生成する。

openapi-generator generate
    -i docs/openapi.yaml # 生成元ファイル
    -o generated-sources/spring_stub # 出力先ディレクトリ
    -g spring
    --additional-properties returnSuccessCode=true

STSを開き、Open Projects from File System ...を選択。生成されたソースコードを読み込ませる。

f:id:ky_yk_d:20190113103609p:plain
ファイルシステムからプロジェクトを開く

STS上のBoot dashboardから、読み込んだプロジェクトを選択して起動する。

f:id:ky_yk_d:20190113105011p:plain
Boot dashboard から起動する

発生するエラー

下記のエラーメッセージがコンソールに表示され、アプリケーションの起動に失敗する。

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2019-01-12 17:17:36.297 ERROR 6665 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'xmlModelPlugin': Lookup method resolution failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [springfox.documentation.schema.XmlModelPlugin] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:262) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1198) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1123) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:541) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:501) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:760) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:869) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:395) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:327) ~[spring-boot-2.0.1.RELEASE.jar:2.0.1.RELEASE]
    at org.openapitools.OpenAPI2SpringBoot.main(OpenAPI2SpringBoot.java:24) ~[classes/:na]
Caused by: java.lang.IllegalStateException: Failed to introspect Class [springfox.documentation.schema.XmlModelPlugin] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:659) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:556) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:541) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:245) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    ... 16 common frames omitted
Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlType
    at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na]
    at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3167) ~[na:na]
    at java.base/java.lang.Class.getDeclaredMethods(Class.java:2310) ~[na:na]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:641) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    ... 19 common frames omitted
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlType
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
    ... 23 common frames omitted

発生原因

OpenAPI Generatorが生成したソースコードが想定するJavaのバージョンが8であるのに対し、MacBook ProにインストールしているJavaのバージョンが 11 であり、Java 9 以降で非推奨となっているjavax.xml.bindのモジュールが解決されなかったため。

Java 9 以降で非推奨になったモジュール群

(エラーの発生しなかった)iMacにインストールしてあったのがJava 8 であったのに対し、MacBook Proでは、以前遊びでJava 11 をインストールしてあった。下記の記事に拠れば、Java 9 以降ではいくつかのモジュールが非推奨となっており、デフォルトで解決されずに例外が発生してしまう。

javax.xml.bind

今回の場合、エラーの原因となっていたのは、javax.xml.bindというモジュールだった。このモジュールも、Java 9 以降で非推奨となっているものだ。エラーメッセージの末尾をみると、確かにClassNotFoundExceptionが発生していることがわかる。

Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlType
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
    ... 23 common frames omitted

改めて、エラーメッセージをみてみると、以下のように、XmlModelPluginクラスでIllegalStateExceptionが発生している。つまり、このXmlModelPluiginが、解決されないjavax.xml.bindモジュールに依存していることにより、今回のエラーは発生していたようだ。

java.lang.IllegalStateException: Failed to introspect Class [springfox.documentation.schema.XmlModelPlugin] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17]

OpenAPI GeneratorとSpringFox

なお、上記のXmlModelPluginSpringFoxの一部であるが、OpenAPI GeneratorのGitHubリポジトリの文書を確認してみると、生成されるSpringのスタブサーバはデフォルトでSpringFoxを利用しているという記載が確かに存在する。

library
    library template (sub-template) to use (Default: spring-boot)
        spring-boot - Spring-boot Server application using the SpringFox integration.
        spring-mvc - Spring-MVC Server application using the SpringFox integration.
        spring-cloud - Spring-Cloud-Feign client with Spring-Boot auto-configured settings.

エラー解消のための対応

Java 8 に戻す……のではなく、前掲の記事にあるように、明示的に「Maven Central 上のアーティファクトを利用するよう依存ライブラリを追加」する。具体的には、下記をpom.xmldependenciesに追加すればよい。

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>

以上の記述を追加してから、Boot dashboardから再起動すると、エラーが発生することなく正常に動作するようになった。ブラウザからAPIにアクセスすると、下記のように表示される。

f:id:ky_yk_d:20190112172255p:plain
http://localhost:8080/detailsにブラウザでアクセスした場合

感想

Javaのバージョンの差異に因って生じるエラーということは思いつかなかった。そもそも自分のマシンにインストールされているJavaのバージョンをあまり意識できていなかった。自宅ではJavaを書くことがほとんどないとはいえ、Javaで飯を食っている人間としては猛省すべき事案だった。「iMacなら動くし、まぁいっか」で済ませてしまわずに、原因を調査したことで左記のことに気づくことができたのが救いか。

今回は、対応として依存モジュールを明示するという方法を書いたが、Java 11 で動作するソースコードを生成できればそれに越したことはない。この点については、そのような変更がOpenAPI Generatorで可能なのかどうかをまだ理解できていないので、また何かわかったら記事にしたいと思う。

OpenAPI GeneratorでRESTful APIの定義書から色々自動生成する

APIの定義を書く:Excel仕様書はもういやだ

RESTful APIを提供するサーバと、そのAPIを利用するクライアント(たとえばSPA)とを並行で開発しようとするとき、まずAPIを定義して、それに基づいてサーバ/クライアント双方の実装を進めようと考えるのは自然だと思う。

そうと決まれば、「API仕様書_20190110.xlsx」と題するファイルを新規作成し、シート別にリソース毎の定義を書き始め・・・てはいけない。せっかくAPIを定義したドキュメントを作成するなら、するのなら、ソースコードの自動生成などの恩恵も受けたい。受けられるはずだ。

少しググってみる。どうやらSwaggerというものを使えばいいらしい。Swaggerに興味を持ったタイミングで、ちょうど書店に平積みになっていた『WEB+DB PRESS Vol.108』の表紙が目に入った。そこには、「スキーマ駆動Web API開発 OpenAPI/GraphQLで仕様からコードもテストも作成」の文字。

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

  • 作者: 中野暁人,山本浩平,大和田純,曽根壮大,ZOZOTOWNリプレースチーム,権守健嗣,茨木暢仁,松井菜穂子,新多真琴,laiso,豊田啓介,藤原俊一郎,牧大輔,向井咲人,大島一将,上川慶,末永恭正,久保田祐史,星北斗,池田拓司,竹馬光太郎,粕谷大輔,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/12/22
  • メディア: 単行本
  • この商品を含むブログを見る

この特集がよかった。この特集では、そもそもOpenAPIがどのような需要に応ずるものなのかから、(この記事では言及しないが)API定義に基づいて自動テストを実行する方法まで、サンプルも用いながら説明しており、入門には最適だった。サンプルコードは下記のGitHubリポジトリで公開されている。

github.com

この特集をきっかけとして、OpenAPI関連のツールを少し使ってみたので、まとめておく。

OAS準拠のAPI定義を書く

「OpenAPI Specification」とは

OpenAPI Specification(OAS)は、RESTful APIを定義するための標準仕様だ。OASにしたがって記述されたJSONあるいはYAMLファイルからは、HTMLドキュメント、スタブサーバやAPIクライアントのソースコードなどを自動生成することができる。テキストファイルなので、Gitなどでバージョン管理をすればdiffを確認することも容易になる。

OASドキュメンテーション

github.com

OASのドキュメントは、上掲のGitHubリポジトリのほか、SwaggerのWebサイトにもまとまったものが存在している。元々、現在のOASのベースとなる仕様はSwaggerという名称で、標準化団体であるOpenAPI Initiativeに所有権が移り、2016年に名称がOASに変更されてからも、開発されてきた周辺ツールの数々はSwagger 〇〇という名称のまま存続している。

インストールなしで使える「Swagger Editor」

Swaggerの周辺ツールのうち、一番わかりやすいのが、Swagger Editorだろう。複数のツールを統合したもので、インストール不要で利用できるオンライン版がすぐ試せる*1。画面の左側にOASに準拠した内容を入力すると右側にリアルタイムでHTMLドキュメントのプレビューが表示されるほか、記述内容に即したスタブサーバやAPIクライアントのソースコードを、上部のメニューから生成することができる。

f:id:ky_yk_d:20190113233854p:plain
Swagger Editor(オンライン版)

VS Code拡張「Swagger Viewer」

簡単な編集であれば、オンライン版のSwagger Editorで事足りるが、実際のアプリケーションのAPIを定義するのであれば普段使っているエディタで編集できた方が都合がいい。そのような需要に対しては、先述の特集記事の中でも紹介されている、Visual Studio Code拡張機能の「Swagger Viewer」が優秀だ。OASの構文エラーを表示してくれるほか、Swagger Editorと同じようにHTMLドキュメントのプレビュー表示にも対応している。

marketplace.visualstudio.com

OpenApi Generatorを使ってみる

Swagger Codegenからフォークした「OpenAPI Generator」

OAS準拠のAPI定義からソースコードやHTMLドキュメントを生成するのは、Swaggerの名を冠するツールの中では「Swagger Codegen」の役割だ。Swagger Editorの画面からソースコードを生成するときにも、このSwagger Codegenが機能している。

Swagger Codegenを利用してもいいのだが、今回は冒頭に掲げた特集で紹介されている「OpenAPI Generator」を利用してみることにする。Swagger Codegenが、かつて仕様としてのSwagger(現在のOAS)を所有していたSmartBear社メンバーを中心に開発されているのに対し、Swagger CodegenからフォークされたOpenAPI Generatorはコミュニティによって開発されている。この辺りの事情については、OpenAPI Generaterのコミッターであり、特集にも寄稿されている @NAKANO_Akihito さんのブログ記事に書かれている。

ackintosh.github.io

OpenAPI Generatorをインストールする

github.com

OpenAPI GeneratorはJava製ツールで、Maven Centralから取得することができる。インストールの方法は様々用意されている。今回は、NPMパッケージ版を利用してみることにした。クライアントの開発でNPMを利用することが多いから、有力な選択肢になるのではないだろうか*2

github.com

インストールは通常のNPMパッケージと同じようにnpm installコマンドを実行すればいい。グローバルインストールしてもいいが、クライアント側のアプリケーションにローカルインストールしておくと、package.jsonのscriptが使えるので便利だと思う。

OpenAPI Generatorを使った自動生成を試す

HTMLドキュメントを生成する

VS CodeのSwagger Viewerでリアルタイムにプレビューすることができるので、YAMLファイルを編集している際はそちらを見ればいい。しかし、APIの定義を見るのにわざわざVS Codeを開くのは面倒だから、手軽に見られる静的なHTMLドキュメントが欲しくなる場面もある。

OASの定義ファイルから、OpenAPI GeneratorでHTMLドキュメントを作成することができる。generateコマンドで、-gオプションの引数をhtmlとすればいい。

openapi-generator generate
     -i docs/openapi.yaml # 定義ファイルの指定
     -o docs/html # 出力先ディレクトリの指定
     -g html 

定義ファイルは長大になるのであえて記載しないが、動作を試したい場合はSwagger Editorにアクセスした際にデフォルトで表示される、Swagger Petstoreをそのままコピーしてみればいい。自分で書いてみたYAMLファイルを元に生成したドキュメントは、以下のようなものになる。

f:id:ky_yk_d:20190113214539p:plain
生成されたHTMLドキュメント

APIクライアントを生成する

APIクライアントの作成にも対応している。HTMLドキュメントを作成するのと同様、generateコマンドを用いる。-gオプションの引数に、生成したい言語/フレームワークを指定してあげればいい。選択できる言語とフレームワークの一覧は、GitHubのREADMEに記載にある通り、多岐にわたっている。特集ではJavaScriptソースコードを作成していたが、下の例では、Angular用のクライアントを生成している。このクライアントの使い方については、後日試してみようと思う。

openapi-generator generate
    -i docs/openapi.yaml
    -o generated-sources/client
    -g typescript-angular
    --additional-properties="ngVersion=7.2.0"

スタブサーバーを生成する

APIの定義中には、exampleとして各データ型の値の例示を含めることができる。この各データ型の値の例を元に組み上げられるレスポンスボディのサンプルは、HTMLドキュメント内で表示されるほか、OpenAPI Generatorで生成したスタブサーバの返却する値としても利用できる。

スタブサーバを用意すれば、クライアントの開発はAPIサーバの実装を待たずに実際の動作を確認しながら進められる。スタブサーバも、OpenAPI Generatorのgenerateコマンドで生成することができる。下記では、Springのソースコードを生成している。

openapi-generator generate
     -i docs/openapi.yaml
     -o ../spring_stub
     -g spring
     --additional-properties returnSuccessCode=true

生成したスタブサーバを動作させるのも難しくはない。STSを開いて、ファイルシステムからプロジェクトを開き、Boot dashboardから起動するだけだ。

f:id:ky_yk_d:20190113103609p:plain
ファイルシステムからプロジェクトを開く

f:id:ky_yk_d:20190113105011p:plain
Boot dashboard から起動する

コンソールで起動が確認できたら、スタブサーバが利用できるようになっている。試しに、ブラウザで今回定義したURIのひとつであるlocalhost:8080/detailsにアクセスすると、下記のようにJSONが取得でき、スタブサーバが動作していることが確認できる。

f:id:ky_yk_d:20190112172255p:plain
5000兆円欲しい

感想

設計書というとExcelという環境で仕事をしてきているので、「YAMLからコードを自動生成する」なんてことはどこか別の世界の営みだと感じてしまう部分があった。また、アプリのソースコードを書くのは楽しいと感じていて、勉強するのもそんなに心理的ハードルがないのに対して、XMLYAMLといった設定ファイルの記述は面白くなさそうなイメージがあり、勉強する際も避けて通りがちだった。

JSONは書く機会がそこそこにあるので、さすがにもう 「気味の悪い拡張子」 だとは思わなくなっているが、YAMLは書いたことがなかったので、いい機会になった。経験を積んでいけば、自動化のメリットも感じられるようになるだろうし、単純に見る機会が増えれば苦手意識もなくなると思う。これからも色々試してみたい。

2019.02.03追記

このブログを書いたあとに公開された資料がとても充実したものだったので、紹介する。この資料は、特集記事の著者の一人である中野さんが勉強会での発表で用いたものだ。本記事もスライドの中で少し登場させていただいている。この場でお礼申しあげたい。

*1:インストール版もある。

*2:とはいえ、あくまでラッパーでしかなく、Java製ツールであることには変わりはないので、JVMが不要になるわけではない。

ドキュメンテーションコメントからプログラミングを考える / 『エンジニアのためのJavadoc再入門講座』を読んだ

『エンジニアのためのJavadoc再入門講座 現場で使えるAPI仕様書の作り方』を読んだ。

エンジニアのためのJavadoc再入門講座 現場で使えるAPI仕様書の作り方

エンジニアのためのJavadoc再入門講座 現場で使えるAPI仕様書の作り方

ソースコードの書き方、コメントの書き方を説明している書籍は、『リーダブルコード』をはじめとして数多く存在するが、ドキュメンテーションコメントに焦点を当てている書籍は珍しい。

タイトルにあるように、この書籍はJavaにおけるドキュメンテーションコメントの仕組みであるJavadocを扱ったものである。内容も、多くはJava、あるいはJavadoc固有の事柄が中心だ。しかし、広くドキュメンテーションコメントの書き方、ひいてはコードの書き方や設計の考え方にも役立つ内容も多く含まれていた。

Javadocとは?

Javadocは、JavaソースコードからAPI仕様書を生成する仕組みだ。クラスやメソッド等の宣言の上部に/** コメント */という形で記載しておいたコメント(ドキュメンテーションコメント)を元に、コマンドひとつでHTML形式のAPI仕様書を生成することができる。書籍から具体例を引用しておく*1

/**
 * <p>指定された顧客名の合致する顧客の一覧を返します。
 * <p>顧客の姓と名を1文字のスペース(U+0020)で結合し、
 * その結果が引数に指定された顧客名に前方一致した場合、
 * その顧客は結果のリストに含まれます。
 * @param name 顧客名
 * @return 顧客の一覧(順不同)。一件も合致しなかった場合は空のリストを返す。
 * @exception IllegalArgumentException 顧客名が{@code null}
 * または空文字列だった場合
 */
public List<Customer> findCustomers(String name)
  throws IllegalArgumentException;

上のコメントを書くことによって、下記のようなHTML文書を生成することができるほか、IDE(例はEclipse)でマウスカーソルを当てた際に表示されるツールチップの情報を充実させることができる。

f:id:ky_yk_d:20190106214017p:plain
生成されるHTML文書

f:id:ky_yk_d:20190106235851p:plain
Eclipseで表示されるツールチップ

なぜ「ドキュメンテーションコメント」か?

この書籍では、ドキュメンテーションコメントを、通常のコメントとは異なる役割を持つものとして捉えている。すなわち、通常のコメントが「人間がそのソースコードを読むための補助情報」であるのに対し、ドキュメンテーションコメントは、「コードから独立したAPI仕様書として読まれる」ものだというのだ。

ドキュメンテーションコメントはAPI仕様書を作成するためのものであり、記述すべきなのは、「仕様」である。もちろん仕様は、クラスやメソッドの名称や引数(シグニチャ)から推測することもできるし、細かい動きが知りたければ、ソースコードを読めば理解することができる。

しかしながら、シグニチャだけでは(特に特殊なケースの)振る舞いについて情報が不十分である場合が多いし、いちいちソースコードを読むのはコストがかかりすぎる。そこで、ドキュメンテーションコメントという形で仕様についての情報を補う必要がある。

ドキュメンテーションコメントの意義の二側面

プログラムの仕様が、ドキュメンテーションコメントによって明確になることは、多くのメリットを生み出す。この書籍では、下記のようなものが挙げられている。

  • クラスの目的が明確になることで、そのクラスの用途がわかり、再利用性が増す
  • クラスの制約が明確になることで、そのクラスを使ってはいけない局面がわかり、無用なバグを減らすことができる
  • 正しく文書化されたクラスは、利用する側の学習コストを低く抑えられる

これらは、一度作られたプログラムに関わるメリットである。しかし、著者は同時に、ドキュメンテーションコメントの記述対象となるプログラムの作成段階で得られるメリットにも言及している。それが下記の3つである。

  • 設計の漏れや抜けを削減できる
  • クラスやメソッドの設計品質が上がる
  • テストケースの設計が行いやすくなる

この後者の側面が、この書籍の面白い部分だと思う。仕様を明確に表現するドキュメンテーションコメントを書く(書こうとする)と、実装者自身の曖昧な理解、設計の漏れや歪さに気づくチャンスが生まれる。コメントを書く段階で仕様を明確にできれば、テストも書きやすくなる*2

優れたプログラマであれば、ドキュメンテーションコメントを書くまでもなく、漏れや曖昧さのない設計・実装をすることができるのかもしれない。しかし、少なくとも僕のような経験の浅いプログラマにとっては、自分が書こうとしているプログラムについて吟味する機会は不可欠だ。この書籍を読むことによって、自分が意識してなかったプログラミングの観点を知ることができた。

ドキュメンテーションコメントのチェックポイント(抜粋)

ドキュメンテーションコメントを書く際のチェックポイントは、第3章・第4章で数多く挙げられている。一部を抜粋して下に記載しておく。

  • メソッドの引数
    • 典型的な「特別な値」を受け取ることは可能か?
    • 定義域内に「穴」となる値は存在しないか?
    • 受け取ったオブジェクトの状態は変更されるか?
  • メドッドの返却値
    • 定義域のすべての範囲に対して、適切な返却値が定義されているか?
    • 返された値を、呼び出し側でチェックする必要はないか?
  • メソッドのスローする例外
    • 例外が発生する状況がわかるようになっているか?
  • インターフェース
    • そのインターフェースがコラボレーションすべきオブジェクトは明確になっているか?
  • クラス
    • クラスの生成や破棄に、特別な手続きは必要ないか?
    • そのクラスは、他の類似のクラスとは異なる振る舞いをしないか?

出版社のHPで公開されている目次も参考になると思う。

www.shoeisha.co.jp

感想

この書籍は、ドキュメンテーションコメントについて書かれたものだ。しかし、ドキュメンテーションコメントを書こうとすることは、先に述べたように、考えるという営みを伴うものであるし、ドキュメンテーションコメント以外の、ソースコード本体や通常のコメントで書くべき事柄を浮き彫りにすることでもある。多くを学び、また学ばなければならないと感じさせられた。

また、この書籍で一つの考え方として紹介されている「契約による設計」についても、理解を深めたいと思った。外見も値段もゴツいバートランド・メイヤーの『オブジェクト指向入門』は、読みたい読みたいと言いながら読めていない。平成のうちに読めるだろうか。

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

*1:14ページ。

*2:テストを書こうとすることで仕様の漏れが見つかるという逆の流れもあると著者は脚注(11ページ)で記載している。テスト駆動開発に関わる勉強会で紹介されていた、フレームワークやライブラリに対してテストケースを書いてみることで、それらの仕様に対する理解を深めるというアプローチにも通ずるところがある。

エンジニアとして本格的に一歩を踏み出した1年間:毎月の読書で2018年を振り返る

この記事は、 write-blog-every-week Advent Calendar 2018 24日目の記事です。

昨日の記事は、KIDANI Akito(@kdnakt)さんでした。

kdnakt.hatenablog.com


2017年4月に新卒・未経験で就職した僕にとって、2018年は、エンジニアとして本格的に一歩を踏み出した年になった。仕事で悩んだり、GitHubに草を生やしたり、ブログを書いたり、今までになく活動的な1年だった。中でも、4月下旬から始めたブログは、

  • きっかけの存在でもあるkakakakakkuさん
  • write-blog-every-week Slackのみなさん

の支えもあって、今週に到るまで毎週1回の更新を保つことができており、生活の一部になりつつある。休日ってブログ書くためにあるんでしたよね。

しかし、今回の記事ではブログを中心に据えることはしない。Advent Calendarの記事ということもあり、ブログを中心に振り返っても良かったのだけど、ブログを書くということについてはこのAdvent Calendarの他のみなさんの記事がすでに十分な知見を提供している。また、今年の1月から4月はブログを書いていなかったので、1年の振り返りとしては相応しくないと思う。

というわけで、今回は2018年の1月からの12ヶ月について、その月に読んだ本の中で印象に残っている本を少しずつ紹介しながら、振り返ってみることにした。いくつかの本については、公開済みの記事を引くに留めていることをご了承願いたい。

各月の書籍リストは下記の通り。

1月 ショーペンハウアー『幸福について』

幸福について―人生論 (新潮文庫)

幸福について―人生論 (新潮文庫)

前年11月から参画したプロジェクトでの仕事にも慣れ、正月休みもあって(職業人としては)弛んでいた時期。「技術書を読まないと」という意識は持ちながらも、勢いに任せて文学作品を読み漁っていた。

ショーペンハウアーのこの書籍は、「それ、本気で言ってます?」という内容も多く、(もとより哲学書は真に受けて生活するようなものではないだろうが)そのまま従えるようなものではない。しかし、端々にギクリとさせられる言葉があり、日常の自分の生活を一歩離れたところから吟味するきっかけを与えてくれる。ショーペンハウアーというと取り付きづらく感じるが、この書籍は下記の文章のように卑近な話題も扱っている。

さて他方において、人間が社交的になるのは、孤独に耐えられず、孤独のなかで自分自身に耐えられないからである。社交を求めるのも、異郷に赴いたり旅に出たりするのも、内面の空虚と倦怠とに駆られるためである。そういう人の精神には、独自な運動をみずから掴むだけの原動力が不足している。だから酒を呑んでその原動力を高めようとする。こうした方法でついには本当の呑んべえになってしまう者が多い。(217ページ)

訳者解説も、短いながら面白い。ピンときた人はぜひ手に取っていただきたい。

幸福は人間の一大迷妄である。蜃気楼である。だがそうは悟れるものでない。この悟れない人間を悟れないままに、幸福の夢を追わせつつ、救済しようというのである。人生はこの意味で、そのまま喜劇である。戯画である。ユーモアである。したがってこれを導く人生論も諷刺的、ユーモア的たらざるをえないではないか。著者の説く一大哲理の背後に、ペロリと出した著者の舌を見のがさないでいただきたい。(「解説」363ページ)

2月 広木大地『エンジニアリング組織論への招待』

こういう書籍があり得るのか、と衝撃を受けた一冊。ある意味では、この本を読んでから今年は始まった。

人文系の大学院を出てエンジニアになった僕は、学生時代の関心事と仕事での関心事が結びつくとは思っていなかった。そんな中で手に取ったこの本には、ソフトウェアの世界の知と、その他の分野の知とが結びつけられるということを教えられた。この本を読んでから、もともと薄っすらと持っていた「組織」*1というものへの関心が、実際の行動(読書や思索)の形を取るようになっていったと思う。

著者の広木さんが、ゆのん(@yunon_phys)さんと一緒にやっているポッドキャスト「EM.FM」も、広木さんの独特のセンスと広範な知識が遺憾無く披露されていて実に面白い。

anchor.fm

このポッドキャストが生まれるきっかけとなった「Engineering Manager Meetup」のOSTの場でホワイトボードを書いていたのが僕だった。「エンジニアリングマネージャーが不人気なのってポッドキャストがないからじゃね」という(確か広木さんの)発言から数日でポッドキャストが始まり、「ゆのんさんの行動力すげえ」と驚嘆したのを覚えている。

3月 市谷聡啓・新井剛『カイゼン・ジャーニー』

間違いなく今年1年を規定した書籍の1つ*2

社会人1年目が終わろうとしており、会社にもすっかり幻滅していた時期に手に取ったこの書籍は、アジャイルというものへの傾倒を決定的にしたとともに、「まず自分が行動する」という今でもしばしば忘れがちになる規範を与えてくれた。

また、著者の市谷さんと新井さんを中心に、この『カイゼン・ジャーニー』という書籍を通じて出会ったたくさんの方々には、彼らの社内外での活動から刺激をもらったり、仕事での悩みを相談させてもらったりと、大変お世話になっている。他の業界にはおそらく少ないであろう、「社外のコミュニティ」というものの素晴らしさを教えてもらった。

エンジニアになって良かったと思っている理由の一つは、コミュニティの存在だ。業務で利用しているOSSのコミュニティはもちろん、日々参加させていただいている勉強会や、ブログを書いたりポッドキャストをやったり同人誌を書いたりという様々なコミュニティが自分の生活を支えている。自分も何がしかの形で貢献していきたいと思う。

4月 Martin Fowler『リファクタリング

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

ようやく技術書らしい技術書。2017年11月に『テスト駆動開発』を読み、「TDDだ!」となってみたものの、実践する機会がなかなかないまま、コードを書く仕事からも離れつつあった時期に読んで、「プログラミングがしたい!」と思わされた本。

Javaのサンプルコードを添えながら、コードの振る舞いを変えずに構造を改善する手法が数多く紹介している。良いコードがわからなければ、悪いコードを見てもそれがどう悪いかがわからない(Code Smellを嗅ぎとれない)。また、壊さずに改善する手法がわからなければ、悪いコードを見つけても対処できない。この書籍は、以上の2点を実例付きで教えてくれる書籍だ。コードが改善されていく様は一種のエンターテインメントでさえある

なお、最近出た原書の第2版では、サンプルコードはJavaScriptになっている。僕はまだ手に取っていないので、古川陽介さんと増田亨さんが言及していたツイートを紹介しておく。

Refactoring: Improving the Design of Existing Code (2nd Edition) (Addison-Wesley Signature Series (Fowler))

Refactoring: Improving the Design of Existing Code (2nd Edition) (Addison-Wesley Signature Series (Fowler))

5月 アビー・コバート『今日からはじめる情報設計』

今日からはじめる情報設計 -センスメイキングするための7ステップ

5月は新しいプロジェクトに配属された月であり、またブログを本格的に書き始めた月でもあった。プロジェクトで使う技術、ブログのネタにする技術を試すのにプライベートの時間を多く割いていたので、あまり多く本を読んでいなかったなかで、目を通していたのがこの書籍。

新しいことを一気に始めた時期で、仕事でもうまく物事が整理できずに苦しんでいたときに、帯の「混乱よ、さようなら」というフレーズに惹かれて、すがるように手に取った。正直、最初に読んだときは、期待に反してあまりピンとこなかったのだけど、今回改めてパラパラとめくってみると、役に立ちそうなことがたくさん書いてある。今回はその中から、「言語的不安定度を減らす」と題する節を紹介する。

 私たちは、使うべき言葉に対して自信があるときもあれば、自信がないときもあります。
 言語的不安定度(linguistic insecurity)とは、自分の言葉が、自分たちの文脈における標準やスタイルに合っていないのではないか、という一般的に見られる不安です。
 協力して作業を進めるために、それに関わるすべての人が理解できるような言葉を使う必要があるのです。(95ページ)

プロジェクトチーム内で言葉の意味を揃えることの重要性は、様々なところで語られている。そんな中で、この書籍が主張していることが面白いのは、自分の言葉の、自分が属するコンテキストの標準へのミスマッチに対する「不安」に着目している点だ。言語が混乱していると、情報伝達の効率が悪化するだけでなく、人に不安を与えることにもなるというのは、チーム作りをする際にも意識しておきたい。

6月 Mary Lynn Manns・Linda Rising『Fearless Change』

Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターン

Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターン

omoiyari.fmで熱く語られていて手に取った本。3月ごろからすっかり「アジャイルかぶれ」になり、「会社にアジャイルの手法を広めてやるぞ!」と意気込んで読んだ。まとまった文量を割いたパターン・ランゲージの紹介に初めて接したのもこの本だった。

この書籍の内容を十分に活かせているとは言えないが、何度も思い出す一節がある。それは、パターン17「やってみる(Just do it)」の冒頭で紹介されている。「会社を救う方法があるのに上司がやらせてくれない」と訴える人に対する、セス・ゴーディンの言葉だ。孫引きになるが、引用する。

 あなたが捜し求めているものは保険だ。もし計画がうまく行かなかった場合に、周囲の非難からあなたを守ってくれる保険を求めているのだ。あなたは誰かが『新製品プロジェクトを立ち上げてもいいよ』『コスト削減計画を実行してもいいよ』と、背中を押してくれるのを待っているのではないか。そうやって承認してもらうことで、失敗したときのリスクから逃れようとしているのだろう。でもね、ことはそんなに望み通りには運ばないよ。
 やってみなさい。 誰かに承認してもらうのを待つということは、失敗したら尻拭いをしてもらおうと考えているということだ。組織の上層部の人たちは、あなたを信頼し、任せることによって背負うリスクなど十分承知の上だ。もし上司が承認したあなたの計画が失敗に終わったら、苦境に陥るのはあなたではなく、上司なのだ。(163ページ)

なお、パターン・ランゲージというものについてこの書籍が取っている立場については、『組織パターン』のJim Coplienから疑義も提示されているということが、omoiyari.fm #40で語られていた。この点については、もうちょっと勉強したいと思っている。

7月 山田ズーニー『あなたの話はなぜ「通じない」のか』

あなたの話はなぜ「通じない」のか (ちくま文庫)

あなたの話はなぜ「通じない」のか (ちくま文庫)

感情的に、今年一番刺さった本。プロジェクトで思うように周囲と連携できず、アジャイル普及という意味でも壁にぶつかって苦しんでいたときに手に取った。この本については既に長大な記事を書いているのでそちらの参照を乞う。

ky-yk-d.hatenablog.com

8月 プラトンプロタゴラス

プロタゴラス―ソフィストたち (岩波文庫)

プロタゴラス―ソフィストたち (岩波文庫)

仕事については気持ちが少し落ち着き、仕事に直結するものではない書籍を読みたくなっていた時期。Growthfactionの方々の活動に触れて、「価値観」というテーマに対する関心が強まった時期でもあった。

初読は2015年。当時、大学院でのゼミでこれに関する題材を扱っていて、それをきっかけに読んだ。その時に、内容を非常に面白く感じ、改めて読み直したのが今年の8月だった。この本についてはブログで直接テーマにはしていないが、下記の記事の脚注で言及していた。

なぜ、人は「大事だ」と思っていることのための行動をとることができず、別の欲求に負けてしまうのか?このテーマを扱っている書籍として、プラトンの『プロタゴラス』があったと思う。

「充実とは価値観が満たされた状態である」? - こまどブログ

扱っているテーマは「徳は教えられるか」という、一見すると現代の我々にはあまり縁がなさそうなものなのだが、ソクラテスプロタゴラスの対話の展開は実にエキサイティングだし、語られる内容も古びていない。以下で引用する岩波文庫版の訳者解説で言及されている「知る」ということへの態度が、自分の怠惰さへの批判に感じられてならない

「悪いとは知りながら……」という言い方には、「知る」という事について甘えがある。ソクラテスのいわゆるパラドクスは、ほんとうに知っているのなら絶対に行なわないはずではないかと、この甘えをきびしく禁止するのである。(「解説」200ページ)

9月 ジェフリー・フェファー『悪いヤツほど出世する』

悪いヤツほど出世する (日経ビジネス人文庫)

悪いヤツほど出世する (日経ビジネス人文庫)

マネジメントの方面への関心が高まり、気分良く組織論やリーダーシップ系の本を読んでいた中で手に取り、反省させられた本。こちらも過去記事の参照を乞う。

ky-yk-d.hatenablog.com

10月 スティーヴン・ガイズ『小さな習慣』

小さな習慣

小さな習慣

kakakakakkuさんがブログで紹介していて読んだ本。9月末にwrite-blog-every-week Slackに参加して、習慣というものについて改めて考えていた時期。

kakakakakku.hatenablog.com

僕は知らなかったのだけど、amazonで60件のレビューがあり、Google検索でもたくさん書評・感想が見つかる、広く読まれている本のようだ。「小さすぎて失敗すらできない習慣」を身につけることを勧める書籍で、その理由づけを(可愛らしい装丁から受け取るイメージに反して)かなり念入りにしている。もちろん、実践の仕方も説明されているし、巻末では「小さな習慣」を身につけるための助けとなるアプリも紹介されている。

Momentum 習慣トラッキング

Momentum 習慣トラッキング

  • Mathias Maehlum
  • 仕事効率化
  • 無料

僕もこの本を読んでから上記のアプリを入れて少しずつ取り入れている。書籍の中でも強調されているように、「これくらいならやってやってもいいか」と思えるほどの小さな習慣は達成しやすいし、達成することで快感を得ることもできる。また、副産物として、アプリの「〇〇日連続達成です!」という表示によって、思ったよりも早く月日が過ぎていることに気づける@year_progressをフォローするのに似た意味合いがあると思う。

11月 クリストファー・アレグザンダー『時を超えた建設の道』

時を超えた建設の道

時を超えた建設の道

「パターン・ランゲージ」の原典の1つ。

GoFデザインパターン、『Fearless Change』、ドメイン駆動設計のパターン、『組織パターン』など様々なところで接する「パターン・ランゲージ」というものが、建築家クリストファー・アレグザンダーにその由来を等しく求めながらも、それぞれに言っていることが異なっているように感じ、もやもやしていた。

先に読んでいた『パターン、Wiki、XP』で、アレグザンダーの思想がそれ自体として複雑な、深みを持ったものだということを学び、これはどうしても原典に当たらないといけないと感じ、手に取ることにした。そのものずばりの『パタン・ランゲージ』という著書もあるが、そちらは原題を『A Pattern Language』と言って、「パターン・ランゲージ」の一つの具体化という意味合いが強いということだったので、理論編に当たる『時を超えた建設の道』を手に取った。

これはとても面白い本で、付箋をベタベタ貼りながら読んだ。まだ、消化しきれていないのだが、「あの本でのパターン・ランゲージについての見解は、アレグザンダーのこういう側面を取り出したものだな」といった形で、原典との距離で様々な論者のパターン・ランゲージ観を比較できるようになったのはよかった。原典が正しい唯一のものだ、という立場を取るつもりはないが、他の全てがそこから出たところの源流を知ることは意義があることだと思う。

12月 Robert C. Martin『Clean Architecture』

Clean Architecture 達人に学ぶソフトウェアの構造と設計

Clean Architecture 達人に学ぶソフトウェアの構造と設計

プロジェクトで設計に携わることになり、少しでもヒントを得られればと手に取った。また、並行でドメイン駆動設計の勉強をしていて、記事などでこの『Clean Architecture』に言及されていることも多かったので、読んでおかないと、と思った。

序文にある「アーキテクチャのルールはどれも同じである!」というフレーズが表しているように、この本では様々な時代における著者のソフトウェア開発の経験から抽出された原則の数々が語られる。記事や書籍で当たり前のように引用される「依存関係逆転の原則」などの設計の原則について、紙幅を割いて説明している書籍はありがたい。また、この業界に入って間もない身には、今と異なる技術的環境の中で開発がどのように行われていたのかを知ることができるのも面白い*3

ざっと読んでみた限りでは、なんだかずっと同じことを様々なレベルで、違う形で言っている本のように感じる。「結局こういうことだよね」と、ざっくり要約をしてしまいたくなるのだが、これは読み手である僕が差異を的確に捉えられていないだけなのだろうと思う。一回読んだだけでも十分に得られるもののある書籍だが、時間を空けて何度も読み返すに足る書籍でもあると思う。

2019年に向けて

こうして振り返ってみると、充実した1年間だったと改めて思う。色んな学びがあり、出会いがあった。来年の今頃どうなっているかが我ながら楽しみになる。

一方で、やるやる言いながらやらなかったこともたくさんある。技術ブログを毎週書く、振り返りをする、登壇する、などもそうだ。8月の記事に書いたことはほとんど嘘になってしまった。気合入れて宣言したことはだいたいやらないというのが自分の弱点だとよくわかった。

また、今回はすごく本を読んだ体の記事になったが、読みたいと思っていたのに結局読まなかった本もたくさんある。軽い本に逃げてしまった場面も多かった。今年読めなかった本の一部を下に示して来年の自分への宿題としたい。


write-blog-every-week Advent Calendar 2018、明日(最終日!)の担当は、今回の Advent Calendar の発起人・よしたく (@yoshitaku_jp)さんです!

adventar.org

*1:この本を手に取ったのも、タイトルの「組織論」というフレーズに惹かれたからだった。

*2:発売は2月だが、物語調であることを敬遠して3月にようやく手に取った。

*3:付録Aは「アーキテクチャ考古学」である。

【Angular】HTTP通信のテストでのインジェクションのエラーの解消

Angular*1のHTTP通信部分(Serviceクラス)のテスト*2を書く際に、派手につまずいたので対応方法と経緯をメモ。

テスト対象の実装

HTTP通信には、AngularのHttpClientを用いている。

実装側のコードにおいては、公式ドキュメントにあるように、通信を使用するModuleクラス*3HttpClientModuleをインポートしたうえで、ServiceクラスにHttpClientをインジェクトする。これでHTTP通信が利用できるようになる。

これのテストコードを書きたい。公式ドキュメントには、モックを用いたテストの方法が書いてあるが、今回は実際のAPI(サーバーサイドの実装)をテストしたいという意味合いもあったので、モックを利用しないテストを書く。

対応方法

結論から先に書くと、テスト側のコードにおいては、下記のように、HttpClientModuleをインポートすればよい。

beforeEach(() => TestBed.configureTestingModule({
  imports: [HttpClientModule]
}));

以上のように記載することで、テスト実行時に正常にHTTP通信を実行することができる。

対応までの経緯

まず、何も考えずにテストケース(itの部分)を記載する。すると、下記のようなエラーになる。

Error: StaticInjectorError(DynamicTestModule)[ConnpassService -> HttpClient]: 
  StaticInjectorError(Platform: core)[ConnpassService -> HttpClient]: 
    NullInjectorError: No provider for HttpClient!

HttpClientのインジェクトに失敗したと言われる。確かに、実装側のServiceクラスは、HttpClientをインジェクトされている。そこで、下記のように書きたくなった。

  beforeEach(() => TestBed.configureTestingModule({
    providers: [HttpClient]
  }));

実行すると、下記のようなエラーとなる。

Error: StaticInjectorError(DynamicTestModule)[HttpClient -> HttpHandler]: 
  StaticInjectorError(Platform: core)[HttpClient -> HttpHandler]: 
    NullInjectorError: No provider for HttpHandler!

インジェクションに失敗しているらしい。「HttpHandlerがないと怒られているのかー」と考え、providersに追加してみる。

beforeEach(() => TestBed.configureTestingModule({
  providers: [HttpClient, HttpHandler]
}));

テストを再実行すると、下記のようなエラーになった。

Expected TypeError: _this.handler.handle is not a function to be null.

このエラーメッセージは苦しい。Googleで検索してもなかなかズバリ答えが見つけられなかった。

そもそも、HttpClientは明示的に実装側でインジェクトしているものなのに対して、HttpHandlerはそうではない。エラーメッセージを見て、何も考えずにprovidersに追加してしまったが、モックを利用しているわけでもないのに実装側で意識していないことをテスト側で意識しなければならないのはどこかがおかしいと思うべきだった。

試行錯誤を繰り返した結果、先述のようにimportsHttpClientModuleを記載したことでエラーが解消した。実装側で、

  • HttpClientModuleをインポート
  • HttpClientをインジェクト

という2段階を踏んでいるのだから、真っ先にHttpClientModuleへの依存を疑うべき事案だった。

感想

依存しているモジュールをimportsに追加するというのは、Angularの仕組み、TestBedクラスの仕組みを理解していればすぐにわかることなのだろう。有識者に聞けばすぐに答えが返ってくると思われるので、Angular Japan User GroupのSlackも活用していきたい。

今回、テストについての情報をGoogleを駆使して検索してみて、うまく答えにたどり着けなかった。エラーメッセージで検索をするのが常套手段だが、英語の記事を含めてあまり的確な情報を獲得することができなかったので、記事として書いておくことにした。

※記事の内容の誤り、もっといい対応方法等があれば、ぜひご指摘ください。

*1:Angularのバージョンは、7.0.4。

*2:Angular CLIで作成したプロジェクトにデフォルトで設定されるJasmine + Karmaによるテスト。

*3:AppModuleが一般的とドキュメントに記載されている。