これは、複数のパートで構成されているチュートリアルの第 8 章です。第 7 章はこちら、概要はこちらから確認できます。
GitHub で完成済みコードを参照するか、ソリューションパッケージをダウンロードできます。
ダウンロード
この章では、アドビの署名コンポーネントで Sling Model(第 6 章で作成したもの)向け単体テストの作成方法について説明します。単体テストは、Java コードの期待される動作を検証するために Java で記述されるビルド時間テストです。各単体テストは一般的に小さく、期待される結果に対するメソッドの成果(または作業のユニット)を検証します。
ここでは、AEM ベストプラクティスおよび以下を使用します。
AEM 向け Cloud Manager は、AEM コードの単体テストのベストプラクティスを推奨および促進するために、単体テストの実施とコード有効範囲ポートを CI/CD パイプラインに統合します。
コードの単体テストはあらゆるコードベースで有益ですが、Cloud Manager を使用している場合は、Cloud Manager で実行できる単体テストを提供して、コード品質のテストや報告機能を活用することが重要です。
最初に、テストの記述と実行をサポートする Maven 依存関係を追加します。必要な依存関係は 4 つです。
- JUnit4
- Mockito テストフレームワーク
- Apache Sling Mocks
- テストフレームワークの wcm.io
JUnit4、Mockito および Sling Mocks の依存関係は、AEM Maven archetype を使用してセットアップする際、プロジェクトに自動追加されます(以下のように、Sling Mocks 依存関係バージョンは更新する必要があります)。
io.wcm テストフレームワークの依存関係を、プロジェクトの
-
これらの依存関係を追加するには、
aem -guides-wknd/pom.xml を開き、<dependencies>..</dependencies> へ移動して、次の依存関係が定義されていることを確認します。io.wcm 依存性を相互に追加する必要があります。JUnit および Mockito の依存関係は、Adobe AEM Maven Archetype によって以前に追加されています。<dependencies> ... <dependency> <groupId>io.wcm</groupId> <artifactId>io.wcm.testing.aem-mock.junit4</artifactId> <version>2.3.2</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.22</version> <scope>test</scope> </dependency> <dependency> <groupId>junit-addons</groupId> <artifactId>junit-addons</artifactId> <version>1.4</version> <scope>test</scope> </dependency> ... </dependencies>
-
<dependencies> ... <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.testing.sling-mock</artifactId> <version>2.3.4</version> <scope>test</scope> </dependency> ... </dependencies>

単体テストは一般的に、Java クラスと 1 対 1 でマッピングします。この章では、署名コンポーネントを支える Sling Model である BylineImpl.java 用に JUnit テストを記述します。
-
テストする Java クラスを右クリックし、新規/その他/Java/JUnit/JUnit テストケースを選択して Eclipse でこれを実行できます。
現在位置が core プロジェクトのコンテキスト内(親 aem-guides-wknd リアクタープロジェクトではなく)であることを確認します。
新しい JUnit テストケース最初のウィザード画面で以下を検証します。
- JUnit テストタイプが New JUnit 4 Test (pom.xml で設定された JUnit Maven 依存関係)となっていること。
- package がテスト対象のクラスの java パッケージ(BylineImpl.java)となっていること。
- ソースフォルダーが core プロジェクトを示し、Eclipse に単体テストファイルの保存先を指定すること。
- setUp() メソッドスタブは手動で作成されること。方法については後で説明します。
- テストの下のクラスが BylineImpl.java であること。これが、テストしたい Java クラスです。
JUnit テストケースウィザード -
ウィザード下部にある「次へ」ボタンをクリックします。
次のステップは、テストメソッドの自動生成に役立ちます。一般的に、Java クラスの各パブリックメソッドには、動作を検証する、対応するテストメソッドが 1 つ以上あります。単体テストには 1 つのパブリックメソッドをテストする複数のテストメソッドがあり、それぞれが異なる入力または状態を表すということがよくあります。
ウィザードで、BylineImpl の下のすべてのメソッドを選択します。ただし、Sling Model によって既に内部で使用されている(@PostConstruct 経由)メソッドである init() を除きます。他のメソッドは正常に実行される init() に依存しているので、その他すべてのメソッドをテストすることで、init() を効果的にテストします。
新しいテストメソッドはいつでも JUnit テストクラスに追加できます。ウィザードのこのページは便宜上のものです。
JUnit テストケースウィザード(続き)
テストファイルには多数の自動生成メソッドがあります。この時点で、この JUnit テストファイルには AEM 固有のものはありません。
最初のメソッドは public void setUp() { ..}で、注釈 @Before が付けられています。
@Before 注釈は、JUnit テスト実行に対し、このクラスで各テストメソッドを実行する前にこのメソッドを実施するよう指示する JUnit 注釈です。
それ以降のメソッドはテストメソッドであり、@Test 注釈でそのようにマークされます。デフォルトでは、すべてのテストが失敗するように設定されています。
この JUnit (または JUniit テストケース)が実行している場合、@Test とマークされた各メソッドは、成功/失敗する可能性があるテストとして実行されます。

単体テストを作成する際の主なアプローチは次の 2 つです。
- TDD またはテスト駆動開発。実装を開発する直前に単体テストの増分を記述、テストを記述、実装を記述してテストを合格します。
- 最初に実装をおこなう開発。動作するコードを最初に開発してから、そのコードを検証するテストを記述します。
このチュートリアルでは、後者のアプローチを使用します(前の章で動作する BylineImpl.java を作成済みのため)。このため、パブリックメソッドの動作だけでなく、いくつかの実装の詳細についても確認および理解しておく必要があります。優れたテストは入力と出力のみを重視する必要があるので、理屈に合わないと思われるかもしれません。AEM で作業する際には、動作するテストを構築するために、実装に関する様々な考慮事項を理解しておく必要があります。
AEM における TDD には高度な専門知識が必要です。AEM 開発や AEM コードの単体テストを熟知した AEM 開発者が使用することで最大限の効果を発揮できます。
AEM で記述されるコードの大部分は JCR、Sling または AEM API に依存しているので、正常に実行するためには実行中の AEM のコンテキストが必要となります。
単体テストは実行中の AEM インスタンスのコンテキスト外にあるビルドで実施されるので、このようなリソースはありません。これを促すために、io.wcm の AEMContext は、これらの API が、ほぼ AEM 内で実行しているかのように動作できるモックコンテキストを作成します。
-
import org.junit.Rule; import io.wcm.testing.mock.aem.junit.AemContext; ... @Rule public final AemContext ctx = new AemContext();
この変数「
ctx 」は、多数の AEM および Sling 抽象を提供するモック AEM コンテキストを提供します。- BylineImpl Sling Model はこのコンテキストに登録されます。
- モック JCR コンテンツ構造はこのコンテキストで作成されます。
- カスタム OSGi サービスはこのコンテキスト内で登録できます。
- 一般的に必要となる様々なモックオブジェクトおよびヘルパー(SlingHttpServletRequest オブジェクトなど)、様々なモック Sling および AEM OSGi サービス(ModelFactory、PageManager、ページ、テンプレート、ComponentManager、コンポーネント、TagManager、タグなど)を提供します。
- これらのオブジェクトのすべてのメソッドが実装されるわけではありません。
- その他
ctx オブジェクトは、ほとんどのモックコンテキストのエントリポイントとして機能します。 -
@Before public void setUp() throws Exception { ctx.addModelsForClasses(BylineImpl.class); ctx.load().json("/com/adobe/aem/guides/wknd/core/components/impl/BylineImplTest.json", "/content"); }
-
モックリソース構造を表す JSON ファイルは、JUnit Java テストファイルと同じパッケージパスに従い、core/src/test/resources 配下に保存されます。
JSON ファイルの命名(BylineImplTest.java)は任意ですが、どの単体テストをサポートしているかが明確になるように名前を付けると良いでしょう。
core/test/resources/com/adobe/aem/guides/wknd/core/components/impl/BylineImplTest.json に新しい JSON ファイルを作成し、次のコンテンツを含めます。
{ "byline": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline" } }
この JSON は、署名コンポーネント単体テストのモックリソースを定義します。この時点で、JSON には、署名コンポーネントコンテンツリソースを表す最小限のプロパティのセットである
jcr :primaryType および sling:resourceType のみが含まれます。単体テストで作業する際の一般的なルールは、各テストに必要な最小限のモックコンテキスト、コンテンツ、およびコードのセットを作成することです。テストを記述する前に、完全なモックコンテキストを構築したくなりますが、不要なアーティファクトが生成される結果になることが多いので、避けてください。
BylineImplTest.json の存在により、ctx.json("/com/adobe/aem/guides/wknd/core/components/impl/BylineImplTest.json", "/content") を実行すると、モックリソース定義はパス /content
にあるコンテキストに読み込まれます。
基本的なモックコンテキストの設定が完了したところで、BylineImpl's getName() の最初のテストを作成しましょう。このテストでは、メソッド getName() がリソースの "name" プロパティに保存されている、作成された正しい名前を返すことを確認する必要があります。
-
import com.adobe.aem.guides.wknd.core.components.Byline; ... @Test public void testGetName() { final String expected = "Jane Doe"; ctx.currentResource("/content/byline"); Byline byline = ctx.request().adaptTo(Byline.class); String actual = byline.getName(); assertEquals(expected, actual); }
3 行目は期待値を設定します。 ここでは、これを "Jane Done" に設定します。
5 行目はコードの評価をおこなうモックリソースのコンテキストを設定します。そのため、これはモック署名コンテンツリソースの読み込み先となる /content/byline に設定されます。
6 行目は、モックリクエストオブジェクトから適応させて署名 Sling Model をインスタンス化します。
8 行目は、署名 Sling Model オブジェクト上で、テストするオブジェクトである getName() を呼び出します。
10 行目は、期待される値が、署名 Sling Model オブジェクトで返される値と一致することをアサートします。これらの値が等しくない場合、テストは失敗します。
-
上記のBylineImpl.java のレビュービデオでは、@PostConstruct init() が例外をスローして Sling Model のインスタンス化を防ぐ方法について説明しました。ここではこれが発生しています。
@PostConstruct private void init() { image = modelFactory.getModelFromWrappedRequest(request, request.getResource(), Image.class); }
ModelFactory OSGi サービスは AemContext 経由で(Apache Sling コンテキストで)提供されますが、BylineImpl の init() メソッドで呼び出される getModelFromWrappedRequest(...) を含み、すべてのメソッドが実装されるわけではないということがわかります。この結果 AbstractMethodError が返されます。つまり、init() が失敗し、結果として適応した
ctx .request().adaptTo(Byline.class) は null オブジェクトとなります。提供されたモックはコードに対応できないので、自分たちでモックコンテキストを実装する必要があります。この場合、Mockito を使用して、getModelFromWrappedRequest(...) が呼び出されたときにモック Image オブジェクトを返すモック ModelFactory オブジェクトを作成できます。
署名 Sling Model を均等にインスタンス化するようこのモックコンテキストを配置する必要があるので、@Before setUp() メソッドに追加できます。また、BylineImpleTest クラスの上に @RunWith(MockitoJUnitRunner.class) 注釈を追加する必要があります。
import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.Mock; import com.adobe.cq.wcm.core.components.models.Image; import org.apache.sling.models.factory.ModelFactory; import static org.mockito.Mockito.*; ... @RunWith(MockitoJUnitRunner.class) public class BylineImplTest { @Rule public final AemContext ctx = new AemContext(); @Mock private Image image; @Mock private ModelFactory modelFactory; @Before public void setUp() throws Exception { ctx.addModelsForClasses(BylineImpl.class); ctx.load().json("/com/adobe/aem/guides/wknd/core/components/impl/BylineImplTest.json", "/content"); when(modelFactory.getModelFromWrappedRequest(eq(ctx.request()), any(Resource.class), eq(Image.class))).thenReturn(image); ctx.registerService(ModelFactory.class, modelFactory, org.osgi.framework.Constants.SERVICE_RANKING, Integer.MAX_VALUE); } ...
8 行目は、テストケースクラスを MockitoJUnitRunner とともに実行するようマークし、クラスレベルでモックオブジェクトを定義する @Mock 注釈の使用を許可します。
14~15 行目は type com.adobe.cq.wcm.core.components.models.Image のモックオブジェクトを作成します。これはクラスレベルで定義され、必要に応じて @Test メソッドの動作を変更できます。
11 行目は ModelFactory のモックオブジェクトを作成します。これは純粋な Mockito モックであり、メソッドは実装されません。これはクラスレベルで定義され、必要にに応じて @Test メソッドの動作を変更できます。
26~28 行目は、getModelFromWrappedRequest(..) がモック ModelFactory オブジェクトで呼び出されたときの動作を登録します。thenReturn (..) では、モック Image オブジェクトを返すよう結果が定義されています。この動作は、最初のパラメーターがctx の リクエストオブジェクトと等しく、2 番目のパラメーターがリソースオブジェクトで、かつ 3 番目のパラメーターがコアコンポーネントの Image クラスである場合にのみ呼び出されます。テストを通じて、ctx.currentResource(...) を、BylineImplTest.json で定義した様々なモックリソースに設定することになるので、どのリソースでも使用できます。30~31 行目は、モック ModelFactory オブジェクトを最高のサービスランキングで AemContext に登録します。BylineImpl の init() で使用される ModelFactory は @OSGiService ModelFactory model フィールド経由で挿入されるので、これが必要になります。AemContext が getModelFromWrappedRequest(..) への呼び出しを処理する モックオブジェクトを挿入するには、そのタイプ(ModelFactory)で最高ランクのサービスとして登録する必要があります。
成功です。最初のテストはうまくいきました。先へ進み、getOccupations() をテストします。モックコンテキストの初期化は @Before setUp() メソッドでおこなわれたので、このテストケースのすべての @Test メソッド(getOccupations() を含む)で利用できるようになります。
このメソッドは、職業プロパティに保存されている職業のリストをアルファベット順(降順)に並べ替えて返します。
-
import java.util.List; import com.google.common.collect.ImmutableList; ... @Test public void testGetOccupations() { List<String> expected = new ImmutableList.Builder<String>() .add("Blogger") .add("Photographer") .add("YouTuber") .build(); ctx.currentResource("/content/byline"); Byline byline = ctx.request().adaptTo(Byline.class); List<String> actual = byline.getOccupations(); assertEquals(expected, actual); }
6~10 行目は期待される結果を定義します。
13 行目は現在のリソースを設定し、コンテキストを /content/byline でモックリソース定義に対して評価します。これにより、モックリソースのコンテキストで BylineImpl.java が実行されるようにします。
14 行目は、モックリクエストオブジェクトから適応させて署名 Sling Model をインスタンス化します。
15 行目は、署名 Sling Model オブジェクト上で、テスト対象オブジェクトである getOccupations() を呼び出します。
17 行目は、期待されているリストが実際のリストと同じであるとアサートします。
-
上記の getName() と同様に、BylineImplTest.json は職業を定義しません。そのため、このテストを実行すると、byline.getOccupations() は空のリストを返し、テストが失敗します。
BylineImplTest.json を更新して職業のリストを含めます。すると、getOccupations() によって職業が並べ替えられていることをテストで検証できるよう、このリストはアルファベット順以外に設定されます。
{ "byline": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline", "name": "Jane Doe", "occupations": ["Photographer", "Blogger", "YouTuber"] } }
最後に isEmpty() メソッドをテストします。
isEmpty() のテストは、様々な条件をテストする必要があり、興味深いものです。BylineImpl.java の isEmpty() メソッドをレビューするには、次の条件をテストする必要があります。
- 名前が空のときに true を返す。
- 職業が null または空のときに true を返す。
- 画像が空または src URL がない場合 true を返す。
- 名前、職業、および Image(
src URL 付き)が存在する場合は false を返す。
これにより、BylineImplTest.json で特定の条件や新しいモックリソース構造をテストする新しいテストメソッドを作成して、これらのテストを駆動させる必要があります。
getName()、getOccupations() および getImage() が空の場合、その状態の期待される動作は isEmpty() 経由でテストされるので、このチェックによってテストをスキップすることができます。
-
最初のテストは、プロパティが設定されていない、まったく新しいコンポーネントの条件をテストします。
BylineImplTest.json に新しいリソース定義を追加し、意味のある名前「empty」を付けます。
{ "byline": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline", "name": "Jane Doe", "occupations": ["Photographer", "Blogger", "YouTuber"] }, "empty": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline" } }
-
次に、必要なデータポイント(名前、職業、または画像)が空となっている場合、isEmpty() が true を返すメソッドのセットを作成します。
各テストで、個別モックリソース定義が使用され、without-name および without-occupations の追加リソース定義を使用して BylineImplTest.json を更新します。
{ "byline": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline", "name": "Jane Doe", "occupations": ["Photographer", "Blogger", "YouTuber"] }, "empty": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline" }, "without-name": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline", "occupations": "[Photographer, Blogger, YouTuber]" }, "without-occupations": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline", "name": "Jane Doe" } }
@Test public void testIsEmpty() { ctx.currentResource("/content/empty"); Byline byline = ctx.request().adaptTo(Byline.class); assertTrue(byline.isEmpty()); } @Test public void testIsEmpty_WithoutName() { ctx.currentResource("/content/without-name"); Byline byline = ctx.request().adaptTo(Byline.class); assertTrue(byline.isEmpty()); } @Test public void testIsEmpty_WithoutOccupations() { ctx.currentResource("/content/without-occupations"); Byline byline = ctx.request().adaptTo(Byline.class); assertTrue(byline.isEmpty()); } @Test public void testIsEmpty_WithoutImage() { ctx.currentResource("/content/byline"); when(modelFactory.getModelFromWrappedRequest(eq(ctx.request()), any(Resource.class), eq(Image.class))).thenReturn(null); Byline byline = ctx.request().adaptTo(Byline.class); assertTrue(byline.isEmpty()); } @Test public void testIsEmpty_WithoutImageSrc() { ctx.currentResource("/content/byline"); when(image.getSrc()).thenReturn(""); Byline byline = ctx.request().adaptTo(Byline.class); assertTrue(byline.isEmpty()); }
1~8 行目は、空のモックリソース定義に対してテストし、isEmpty() が true であることをアサートする testIsEmpty() を定義します。
10~17 行目は、職業があるけれども名前がないモックリソース定義をテストする、testIsEmpty_WithoutName() を定義します。
19~26 行目は、名前があるけれども職業がないモックリソース定義をテストする、testIsEmpty_WithoutOccupations() を定義します。
28~39 行目は、名前と職業でモックリソース定義をテストし、モック Image が null を返すよう設定する testIsEmpty_WithoutImage() を定義します。アドビでは、この呼び出しで返される Image が null となるよう、modelFactory.getModelFromWrappedRequest(..)動作(
setUp () で定義したもの)をオーバーライドします。41~50 行目は、名前と職業でモックリソース定義をテストし、getSrc() を呼び出すと空白の文字列を返すようモック Image を設定する
testIsEmpty_WithoutImageSrc() を定義します。 -
最後に、コンポーネントが正しく設定されている場合、isEmpty() が false を返すようテストを記述します。この条件の場合は、完全に設定された署名コンポーネントを表す /content/byline を再使用できます。
@Test public void testIsNotEmpty() { ctx.currentResource("/content/byline"); when(image.getSrc()).thenReturn("/content/bio.png"); Byline byline = ctx.request().adaptTo(Byline.class); assertFalse(byline.isEmpty()); }
コードの有効範囲とは、単体テストの対象となるソースコードの量のことです。最近の IDE は、単体テストでどのソースコードが実行されたかを自動的にチェックするツールを提供します。コードの有効範囲自体はコード品質を表すものではありませんが、単体テストの対象とならない、ソースコードの重要な領域があるかどうかを理解するのに役立ちます。
-
これはリソースに職業の値がない場合、空のリストを返すようアサートする getOccupations() にテストを追加することで修正できます。次の新しいテストメソッドを BylineImplTests.java に追加します。
@Test public void testGetOccupations_WithoutOccupations() { List<String> expected = Collections.emptyList(); ctx.currentResource("/content/empty"); Byline byline = ctx.request().adaptTo(Byline.class); List<String> actual = byline.getOccupations(); assertEquals(expected, actual); }
-
この問題は、職業を空のアレイに設定するモックリソース定義を使用する別のテストメソッドを作成することで簡単に解決できます。
BylineImplTest.jsonに新しいモックリソース定義("without-occupations" のコピー)空のアレイに設定された職業プロパティを追加し、"without-occupations-empty-array"
と名付けます。"without-occupations-empty-array": { "jcr:primaryType": "nt:unstructured", "sling:resourceType": "wknd/components/content/byline", "name": "Jane Doe", "occupations": [] }
@Test public void testIsEmpty_WithEmptyArrayOfOccupations() { ctx.currentResource("/content/without-occupations-empty-array"); Byline byline = ctx.request().adaptTo(Byline.class); assertTrue(byline.isEmpty()); }
testIsEmpty_WithEmptyArrayOfOccupations() の有効範囲
単体テストは、Maven ビルドの一部として実行し、成功する必要があります。これにより、アプリケーションをデプロイする前にすべてのテストが成功することを確認します。 パッケージやインストールなどの Maven 目標を実行すると、テストが自動的に呼び出され、プロジェクトのすべての単体テストで成功する必要があります。
$ mvn package


行き詰まったり、追加の質問がある場合は、AEM 用 Experience League フォーラムを確認するか、既存の GitHub の問題を参照してください。
探していた情報が見つからなかった場合やエラーが見つかった場合は、WKND プロジェクトの問題として GitHub で報告してください。
ダウンロード