(この記事は、開発元Parasoft社 Blog 「Love Spring Testing Even More with Mocking and Unit Test Assistant 」2017年12月20日の翻訳記事です。)
Spring Framework (およびSpringBoot)は、Spring コントローラーのJUnitテストを作成するための便利なテストフレームワークを備えています。私の以前の記事 では、Parasoft Jtestの単体テストアシスタント を使用して、これらのSpringテストを効率的に構築し、改善する方法について説明しました。 その続きとして、この記事では、複雑なアプリケーションをテストする際に必ず課題となる 依存関係の制御 について説明します。
モック化が必要な理由
単刀直入に言いましょう。複雑なアプリケーションをゼロから構築することはありません―ライブラリやAPI、あるいは他の誰かによってメンテナンスされているプロジェクトやサービスを利用していることでしょう。 Springを利用する開発者は、できるだけ既設の機能を利用して、本当の関心の対象―つまりアプリケーションのビジネスロジックに時間と労力を費やすことができます。その一方で細かな部分を ライブラリに任せているので、そのアプリケーションには多くの依存関係が発生します (下の図では依存先コンポーネントがオレンジ色で示されています):
図1. 複数の依存関係を持つSpringサービス
では、機能の大部分を依存するコンポーネントの振る舞いに頼っている場合、どうしたら自分のアプリケーション(コントローラーとサービス)のユニットテスト(単体テスト)に集中できるでしょうか? 結局、ユニットテストではなく統合テストを行っているということにはならないでしょうか? 依存先コンポーネントの動作をより詳細に制御する必要がある場合、あるいはユニットテスト時に依存先コンポーネントを使用できない場合はどうすればよいのでしょう?
必要なのは、アプリケーションコードにユニットテストを集中させることができるよう、アプリケーションを依存関係から切り離すことです。 場合によっては、依存先コンポーネントの特殊な「テスト」バージョンを作成することがあるかもしれません。 しかし、以下のような理由により、Mockitoのような標準化されたライブラリを使用するほうが有利です。
特殊な「テスト」バージョンのコードを自分で作成して管理する必要がない
モック化ライブラリはモックに対する呼び出しを追跡できるため、検証を強化することができる
PowerMockのような標準ライブラリには、静的メソッド、プライベートメソッド、またはコンストラクターのモック化などの追加機能がある
Mockitoのようなモック化ライブラリの知識は、プロジェクト全体で再利用できるのに対して、特殊な「テスト」コードの知識は再利用できない
図2. モック化されたサービスによる複数の依存関係の置き換え
Springの依存関係
一般的に、Springアプリケーションの機能は複数のBeanに分割されます。 コントローラーはService Beanに依存し、Service BeanはEntityManager、JDBC接続、または他のBeanに依存するでしょう。 ほとんどの場合、テスト対象のコードと分離する必要があるのはBeanです。 統合テストであれば、すべてのレイヤーで実際のコンポーネントを使用することにも意味がありますが、ユニットテストでは、どの依存関係を実際のコンポーネントにし、どれをモックにするべきかを決める必要があります。
Springでは、XML、Java、またはその両方の組み合わせを使用してBeanを定義し、構成することができ、モック化されたBeanと実際のBeanを混在させることができます。 モックオブジェクトはJavaで定義する必要があるため、Configurationクラスを使用してモック化されたBeanを定義し、構成します。
依存関係のモック化
単体テストアシスタント(UTA)でSpringテストを生成すると、コントローラーのすべての依存関係がモックとして設定されるため、各テストで依存関係を制御できます。 テストが実行されると、UTAではモックオブジェクトに対するメソッド呼び出しを検査し、まだモック化されていないメソッドを検出した場合、それらのメソッドをモック化すべきだと提案します。 そのとき、即時修正(クイックフィックス機能)を使用すれば各メソッドを自動的にモック化することができます。
PersonServiceに 依存するコントローラーの例を次に示します 。
@Controller
@RequestMapping("/people" )
public class PeopleController {
@Autowired
protected PersonService personService ;
@GetMapping
public ModelAndView people(Model model){
for (Person person : personService .getAllPeople()) {
model.addAttribute(person.getName(), person.getAge());
}
return new ModelAndView("people.jsp" , model.asMap());
}
}
Parasoft Jtestの単体テストアシスタントが生成するテストのサンプル:
@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration
public class PeopleControllerTest {
@Autowired
PersonService personService ;
// Other fields and setup
@Configuration
static class Config {
// Other beans
@Bean
public PersonService getPersonService() {
return mock (PersonService.class );
}
}
@Test
public void testPeople() throws Exception {
// When
ResultActions actions = mockMvc.perform(get ("/people" ));
}
}
ここでは、 @Configuration アノテーションが付加された内部クラスを使用します。 これは、Javaコンフィギュレーションを使用してテスト対象のControllerにBean依存依存関係を提供します。 これにより、BeanメソッドでPersonService をモック化できます。 メソッドはまだモック化されていないので、テストを実行すると、次のように推奨事項が表示されます:
これは、モック化されたPersonService でgetAllPeople() メソッドが呼び出されたが、テストではまだこのメソッドのモック化を設定していないことを意味しています。 「Mock it」即時修正オプションを選択すると、テストが更新されます:
@Test
public void testPeople() throws Exception {
Collection<Person> getAllPeopleResult = new ArrayList<Person>();
doReturn (getAllPeopleResult).when(personService ).getAllPeople();
// When
ResultActions actions = mockMvc .perform(get ("/people" ));
もう一度テストを実行すると、テストは合格します。 まだgetAllPeople() によって返されるCollection に値を設定する必要がありますが、モック化された依存関係を設定するという課題は解決されています。
生成されたメソッドのモック化をテストメソッドからConfigurationクラスのBeanメソッドに移動することができます。 こうすると、クラス内の各テストが同じメソッドを同じようにモック化することになります。 メソッドのモック化をテストメソッドに残すと、テスト毎に異なる方法でメソッドをモック化できます。
Spring Boot
Spring Bootでは、Beanのモック化はもっと簡単です。 テストでBeanに@Autowired フィールドを使用し、ConfigurationクラスでBeanを定義する代わりに、Beanのフィールドを使用して@MockBean アノテーションを付加することができます。 Spring Bootは、クラスパス上で見つかったモックフレームワークを使ってBeanのモックを作成し、コンテナー内の他のBeanを注入するのと同じ方法で注入します。 単体テストアシスタント(UTA)でSpring Bootテストを生成すると、Configurationクラスではなく@MockBean 機能が使用されます。
@SpringBootTest
@AutoConfigureMockMvc
public class PeopleControllerTest {
// Other fields and setup – no Configuration class needed!
@MockBean
PersonService personService ;
@Test
public void testPeople() throws Exception {
...
}
}
XMLコンフィギュレーションとJavaコンフィギュレーション
上記の最初の例では、ConfigurationクラスはSpringコンテナーにすべてのBeanを提供していました。 別の方法として、ConfigurationクラスではなくXMLコンフィギュレーションを使用できます。または2つを組み合わせることもできます。 例:
@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration({ "classpath:/**/testContext.xml" })
public class PeopleControllerTest {
@Autowired
PersonService personService ;
// Other fields and setup
@Configuration
static class Config {
@Bean
@Primary
public PersonService getPersonService() {
return mock (PersonService.class );
}
}
// Tests
}
この例では、クラスは@ContextConfiguration アノテーション(ここでは示していません)内のXMLコンフィギュレーションファイルを参照して、ほとんどのBean(実際のBeanまたはテスト固有のBean)を提供します。 PersonService をモック化する@Configuration クラスもあります。@Primary アノテーションは、XMLコンフィギュレーションでPersonService Beanが見つかった場合でも、@Configuration クラスのモック化された Beanのほうを使用するよう指示するものです。このような構成により、テストコードを小さくして管理しやすくできます。
必要な任意の@ContextConfiguration 属性を使用してテストを生成するようにUTAを設定できます。
静的メソッドのモック化
依存先コンポーネントに静的にアクセスする場合があります。 例えば、アプリケーションは、静的メソッドの呼び出しを通じてサードパーティのサービスにアクセスすることがあります。
public class ExternalPersonService {
public static Person getPerson(int id) {
RestTemplate restTemplate = new RestTemplate();
try {
return restTemplate.getForObject("http://domain.com/people/" + id, Person.class );
} catch (RestClientException e) {
return null ;
}
}
}
サンプルのコントローラー:
@GetMapping
public ResponseEntity<Person> getPerson(@PathVariable("id" ) int id, Model model)
{
Person person = ExternalPersonService.getPerson (id);
if (person != null ) {
return new ResponseEntity<Person>(person, HttpStatus.OK );
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND );
}
この例では、ハンドラーメソッドは静的メソッド呼び出しを使用して、サードパーティのサービスからPersonオブジェクトを取得します。 このハンドラーメソッドのJUnitテストをビルドすると、テストが実行されるたびに実際のHTTP呼び出しがサービスに送信されます。
代わりに、静的なExternalPersonService.getPerson() メソッドをモック化します。 これにより、HTTP呼び出しを回避し、テストのニーズに合ったPerson レスポンスオブジェクトを提供することができます。 単体テストアシスタントを使用すると、静的メソッドをPowerMockitoで簡単にモック化できます。
UTAは上記のハンドラーメソッドに対して次のようなテストを生成します:
@Test
public void testGetPerson() throws Throwable {
// When
long id = 1L;
ResultActions actions = mockMvc .perform(get ("/people/" + id));
// Then
actions.andExpect(status ().isOk());
}
テストを実行すると、HTTP呼び出しが行われるのをUTA のフロー ツリーで参照できます。 ExternalPersonService.getPerson()の 呼び出しを見つけてモック化します:
テストが更新され、PowerMockを使用して静的メソッドがモック化されます。
@Test
public void testGetPerson() throws Throwable {
spy (ExternalPersonService.class );
Person getPersonResult = null ; // UTA: default value
doReturn (getPersonResult).when(ExternalPersonService.class , "getPerson" ,anyInt ());
// When
int id = 0;
ResultActions actions = mockMvc .perform(get ("/people/" + id));
// Then
actions.andExpect(status ().isOk());
}
UTAを使用して、 getPersonResult 変数を選択してインスタンス化できるようになりました。したがって 、モック化されたメソッドの呼び出しはnull を返しません。
String name = ""; // UTA: default value
int age = 0; // UTA: default value
Person getPersonResult = new Person(name, age);
もう一度テストを実行すると、モック化されたExternalPersonService.getPerson() メソッドからgetPersonResult が返され、テストに合格します。
注意: フローツリーからは、静的メソッドの呼び出しに対して [Add Mockable Method pattern] を選択することもできます。これにより、新しいテストを生成するときは、常に静的メソッドの呼び出しをモック化するように単体テストアシスタントが設定されます。
結論
複雑なアプリケーションは機能的な依存関係を持つことがよくあります。そのことは、開発者がコードのユニットテストを行うことを難しく、限定的にします。 Mockitoのようなモックフレームワークを使用することで、依存関係からコードを分離し、素早くユニットテストを作成できます。 単体テストアシスタントは、新しいテストを作成してモックを設定し、また実行時にモック化されていないメソッドを見つけて、開発者がモックを生成するのを支援します。これにより、依存関係の制御が容易になります。
Parasoft Jtestについて
Java対応静的解析・単体テストツール Parasoft Jtest
Jtestは、テスト工数の大幅削減とセキュアで高品質なJavaシステムの開発を強力にサポートするJava対応テストツールです。1,000個以上のコーディング規約をもとにソースコードを静的に解析し、プログラムの問題点や処理フローに潜む検出困難なエラーを検出します。さらに、JUnitを用いた単体テストについて、作成、実行、テストカバレッジ分析、テスト資産の管理といった単体テストに係る作業をサポートし、単体テストの効率化を促進します。