ウォーターフォールモデルからアジャイル、そして今ではDevOpsに移行するテクノロジーと業界の進歩に伴い、アプリケーションの変更と拡張機能は、作成された瞬間に本番環境に展開されます。コードがこれほど速く本番環境にデプロイされるので、変更が機能し、既存の機能が損なわれないことを確信する必要があります。
この自信を築くには、 フレームワーク 自動回帰テスト用。回帰テストを実行するには、APIレベルの観点から実行する必要のあるテストが多数ありますが、ここでは2つの主要なタイプのテストについて説明します。
デザインの要素と原則
すべてのプログラミング言語で利用できるフレームワークは多数あります。で書かれたウェブアプリのユニットと統合テストの作成に焦点を当てます Java のSpringフレームワーク。
ほとんどの場合、クラスにメソッドを記述し、これらは他のクラスのメソッドと相互作用します。今日の世界、特に エンタープライズアプリケーション —アプリケーションの複雑さは、単一のメソッドが複数のクラスの複数のメソッドを呼び出す可能性があるようなものです。したがって、そのようなメソッドの単体テストを作成するときは、それらの呼び出しからモックされたデータを返す方法が必要です。これは、この単体テストの目的が1つのメソッドのみをテストすることであり、この特定のメソッドが行うすべての呼び出しをテストすることではないためです。
JUnitフレームワークを使用してSpringでのJavaユニットテストに飛び込みましょう。聞いたことがあるかもしれない何かから始めましょう:モック。
関数CalculateArea
を持つクラスcalculateArea(Type type, Double... args)
があるとします。これは、指定されたタイプ(円、正方形、または長方形)の形状の面積を計算します。
依存性注入を使用しない通常のアプリケーションでは、コードは次のようになります。
public class CalculateArea { SquareService squareService; RectangleService rectangleService; CircleService circleService; CalculateArea(SquareService squareService, RectangleService rectangeService, CircleService circleService) { this.squareService = squareService; this.rectangleService = rectangeService; this.circleService = circleService; } public Double calculateArea(Type type, Double... r ) { switch (type) { case RECTANGLE: if(r.length >=2) return rectangleService.area(r[0],r[1]); else throw new RuntimeException('Missing required params'); case SQUARE: if(r.length >=1) return squareService.area(r[0]); else throw new RuntimeException('Missing required param'); case CIRCLE: if(r.length >=1) return circleService.area(r[0]); else throw new RuntimeException('Missing required param'); default: throw new RuntimeException('Operation not supported'); } } }
public class SquareService { public Double area(double r) { return r * r; } }
public class RectangleService { public Double area(Double r, Double h) { return r * h; } }
public class CircleService { public Double area(Double r) { return Math.PI * r * r; } }
public enum Type { RECTANGLE,SQUARE,CIRCLE; }
ここで、関数をユニットテストする場合calculateArea()
クラスCalculateArea
の場合、私たちの動機はswitch
かどうかを確認することです。ケースと例外条件は機能します。シェイプサービスが正しい値を返しているかどうかをテストするべきではありません。前述のように、関数の単体テストの目的は、関数が行っている呼び出しのロジックではなく、関数のロジックをテストすることです。
したがって、個々のサービス関数(rectangleService.area()
など)によって返される値をモックし、それらのモックされた値に基づいて呼び出し元の関数(CalculateArea.calculateArea()
など)をテストします。
長方形サービスの簡単なテストケース—それをテストするcalculateArea()
確かにrectangleService.area()
を呼び出します正しいパラメータを使用すると、次のようになります。
import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; public class CalculateAreaTest { RectangleService rectangleService; SquareService squareService; CircleService circleService; CalculateArea calculateArea; @Before public void init() { rectangleService = Mockito.mock(RectangleService.class); squareService = Mockito.mock(SquareService.class); circleService = Mockito.mock(CircleService.class); calculateArea = new CalculateArea(squareService,rectangleService,circleService); } @Test public void calculateRectangleAreaTest() { Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d); Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d); Assert.assertEquals(new Double(20d),calculatedArea); } }
ここで注意すべき2つの主要な行は次のとおりです。
rectangleService = Mockito.mock(RectangleService.class);
—これにより、実際のオブジェクトではなく、モックされたオブジェクトであるモックが作成されます。Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
—これは、嘲笑されたとき、そしてrectangleService
オブジェクトのarea
指定されたパラメーターを使用してメソッドが呼び出され、20d
が返されます。さて、上記のコードがSpringアプリケーションの一部である場合はどうなりますか?
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class CalculateArea { SquareService squareService; RectangleService rectangleService; CircleService circleService; public CalculateArea(@Autowired SquareService squareService, @Autowired RectangleService rectangeService, @Autowired CircleService circleService) { this.squareService = squareService; this.rectangleService = rectangeService; this.circleService = circleService; } public Double calculateArea(Type type, Double... r ) { // (same implementation as before) } }
ここに、コンテキストの初期化時に検出する、基盤となるSpringフレームワークの2つのアノテーションがあります。
@Component
:タイプCalculateArea
のBeanを作成します@Autowired
:Beanを検索しますrectangleService
、squareService
、およびcircleService
そしてそれらを豆に注入しますcalculatedArea
同様に、他のクラスのBeanも作成します。
import org.springframework.stereotype.Service; @Service public class SquareService { public Double area(double r) { return r*r; } }
import org.springframework.stereotype.Service; @Service public class CircleService { public Double area(Double r) { return Math.PI * r * r; } }
import org.springframework.stereotype.Service; @Service public class RectangleService { public Double area(Double r, Double h) { return r*h; } }
ここでテストを実行すると、結果は同じになります。ここではコンストラクタインジェクションを使用しましたが、幸い、テストケースを変更しないでください。
ただし、正方形、円、および長方形のサービスのBeanを注入する別の方法があります。フィールド注入です。それを使用する場合、テストケースにいくつかの小さな変更が必要になります。
記事の範囲外であるため、どちらの注入メカニズムが優れているかについては説明しません。しかし、私たちはこれを言うことができます:Beanを注入するために使用するメカニズムのタイプに関係なく、それに対するJUnitテストを作成する方法は常にあります。
フィールドインジェクションの場合、コードは次のようになります。
@Component public class CalculateArea { @Autowired SquareService squareService; @Autowired RectangleService rectangleService; @Autowired CircleService circleService; public Double calculateArea(Type type, Double... r ) { // (same implementation as before) } }
注:フィールドインジェクションを使用しているため、パラメーター化されたコンストラクターは必要ありません。したがって、オブジェクトはデフォルトのコンストラクターを使用して作成され、値はフィールドインジェクションメカニズムを使用して設定されます。
サービスクラスのコードは上記と同じですが、テストクラスのコードは次のとおりです。
public class CalculateAreaTest { @Mock RectangleService rectangleService; @Mock SquareService squareService; @Mock CircleService circleService; @InjectMocks CalculateArea calculateArea; @Before public void init() { MockitoAnnotations.initMocks(this); } @Test public void calculateRectangleAreaTest() { Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d); Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d); Assert.assertEquals(new Double(20d),calculatedArea); } }
ここではいくつかの点が異なります。基本ではなく、それを達成する方法です。
まず、オブジェクトをモックする方法:@Mock
を使用しますinitMocks()
とともに注釈モックを作成します。次に、@InjectMocks
を使用して実際のオブジェクトにモックを注入します。 initMocks()
と一緒に。
これは、コードの行数を減らすために行われます。
上記のサンプルでは、すべてのテストを実行するために使用される基本的なランナーはBlockJUnit4ClassRunner
です。これはすべての注釈を検出し、それに応じてすべてのテストを実行します。
さらに機能が必要な場合は、カスタムランナーを作成できます。たとえば、上記のテストクラスで、MockitoAnnotations.initMocks(this);
の行をスキップする場合次に、BlockJUnit4ClassRunner
の上に構築された別のランナーを使用できます。 MockitoJUnitRunner
。
MockitoJUnitRunner
を使用すると、モックを初期化して注入する必要もありません。それはMockitoJUnitRunner
によって行われます注釈を読むだけでそれ自体。
(Springアプリケーションの起動時にSpringJUnit4ClassRunner
が作成されるのと同じように、Spring統合テストに必要なApplicationContext
を初期化するApplicationContext
もあります。これについては後で説明します。)
テストクラスのオブジェクトがいくつかのメソッドをモックするだけでなく、いくつかの実際のメソッドを呼び出すようにする場合は、部分的なモックが必要です。これは@Spy
を介して達成されますJUnitで。
@Mock
を使用するのとは異なり、@Spy
を使用すると、実際のオブジェクトが作成されますが、そのオブジェクトのメソッドは、モックしたり、実際に呼び出すことができます。
たとえば、area
の場合クラスのメソッドRectangleService
追加のメソッドを呼び出すlog()
実際にそのログを印刷したい場合、コードは次のように変更されます。
@Service public class RectangleService { public Double area(Double r, Double h) { log(); return r*h; } public void log() { System.out.println('skip this'); } }
@Mock
を変更した場合rectangleService
の注釈@Spy
に移動し、以下に示すようにコードを変更すると、結果では実際にログが出力されますが、メソッドarea()
嘲笑されます。つまり、元の関数はその副作用のためだけに実行されます。その戻り値はモックされたものに置き換えられます。
@RunWith(MockitoJUnitRunner.class) public class CalculateAreaTest { @Spy RectangleService rectangleService; @Mock SquareService squareService; @Mock CircleService circleService; @InjectMocks CalculateArea calculateArea; @Test public void calculateRectangleAreaTest() { Mockito.doCallRealMethod().when(rectangleService).log(); Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d); Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d); Assert.assertEquals(new Double(20d),calculatedArea); } }
Controller
のテストについてどのように進めますかまたはRequestHandler
?上で学んだことから、この例のコントローラーのテストコードは次のようになります。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class AreaController { @Autowired CalculateArea calculateArea; @RequestMapping(value = 'api/area', method = RequestMethod.GET) @ResponseBody public ResponseEntity calculateArea( @RequestParam('type') String type, @RequestParam('param1') String param1, @RequestParam(value = 'param2', required = false) String param2 ) { try { Double area = calculateArea.calculateArea( Type.valueOf(type), Double.parseDouble(param1), Double.parseDouble(param2) ); return new ResponseEntity(area, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity(e.getCause(), HttpStatus.INTERNAL_SERVER_ERROR); } } }
import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @RunWith(MockitoJUnitRunner.class) public class AreaControllerTest { @Mock CalculateArea calculateArea; @InjectMocks AreaController areaController; @Test public void calculateAreaTest() { Mockito .when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d)) .thenReturn(20d); ResponseEntity responseEntity = areaController.calculateArea('RECTANGLE', '5', '4'); Assert.assertEquals(HttpStatus.OK,responseEntity.getStatusCode()); Assert.assertEquals(20d,responseEntity.getBody()); } }
上記のコントローラーテストコードを見ると、正常に機能しますが、基本的な問題が1つあります。それは、実際のAPI呼び出しではなく、メソッド呼び出しのみをテストすることです。 APIパラメータとAPI呼び出しのステータスをさまざまな入力についてテストする必要があるすべてのテストケースがありません。
このコードは優れています:
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; @RunWith(SpringJUnit4ClassRunner.class) public class AreaControllerTest { @Mock CalculateArea calculateArea; @InjectMocks AreaController areaController; MockMvc mockMvc; @Before public void init() { mockMvc = standaloneSetup(areaController).build(); } @Test public void calculateAreaTest() throws Exception { Mockito .when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d)) .thenReturn(20d); mockMvc.perform( MockMvcRequestBuilders.get('/api/area?type=RECTANGLE¶m1=5¶m2=4') ) .andExpect(status().isOk()) .andExpect(content().string('20.0')); } }
ここでは、MockMvc
の方法を確認できます。実際のAPI呼び出しを実行する仕事を引き受けます。 status()
のような特別なマッチャーもありますおよびcontent()
これにより、コンテンツの検証が容易になります。
コードの個々のユニットが機能することがわかったので、期待どおりに相互作用することを確認しましょう。
まず、すべてのBeanをインスタンス化する必要があります。これは、アプリケーションの起動時にSpringコンテキストの初期化時に発生するのと同じことです。
このために、クラス内のすべてのBeanを定義します。たとえば、TestConfig.java
:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class TestConfig { @Bean public AreaController areaController() { return new AreaController(); } @Bean public CalculateArea calculateArea() { return new CalculateArea(); } @Bean public RectangleService rectangleService() { return new RectangleService(); } @Bean public SquareService squareService() { return new SquareService(); } @Bean public CircleService circleService() { return new CircleService(); } }
それでは、このクラスをどのように使用し、統合テストを作成するかを見てみましょう。
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestConfig.class}) public class AreaControllerIntegrationTest { @Autowired AreaController areaController; MockMvc mockMvc; @Before public void init() { mockMvc = standaloneSetup(areaController).build(); } @Test public void calculateAreaTest() throws Exception { mockMvc.perform( MockMvcRequestBuilders.get('/api/area?type=RECTANGLE¶m1=5¶m2=4') ) .andExpect(status().isOk()) .andExpect(content().string('20.0')); } }
ここでいくつかの変更があります。
@ContextConfiguration(classes = {TestConfig.class})
—これは、すべてのBean定義が存在するテストケースを示します。@InjectMocks
の代わりにを使用しております: @Autowired AreaController areaController;
他のすべては同じままです。テストをデバッグすると、コードは実際にはarea()
の最後の行まで実行されることがわかります。 RectangleService
のメソッドここでreturn r*h
計算されます。つまり、実際のビジネスロジックが実行されます。
これは、統合テストで使用可能なメソッド呼び出しまたはデータベース呼び出しのモックがないことを意味するものではありません。上記の例では、サードパーティのサービスやデータベースが使用されていないため、モックを使用する必要はありませんでした。実生活では、このようなアプリケーションはまれであり、データベースまたはサードパーティのAPI、あるいはその両方にアクセスすることがよくあります。その場合、TestConfig
でBeanを作成するとクラスでは、実際のオブジェクトではなく、モックされたオブジェクトを作成し、必要に応じて使用します。
多くの場合、バックエンド開発者がユニットテストや統合テストを作成するのを妨げるのは、すべてのテストのために準備する必要のあるテストデータです。
通常、データが十分に小さく、1つまたは2つの変数がある場合は、テストデータクラスのオブジェクトを作成していくつかの値を割り当てるだけで簡単です。
たとえば、モックされたオブジェクトが別のオブジェクトを返すことを期待している場合、モックされたオブジェクトで関数が呼び出されると、次のようになります。
Class1 object = new Class1(); object.setVariable1(1); object.setVariable2(2);
そして、このオブジェクトを使用するには、次のようにします。
コンサルティング率の計算方法
Mockito.when(service.method(arguments...)).thenReturn(object);
これは上記のJUnitの例では問題ありませんが、上記のメンバー変数がClass1
の場合クラスは増え続け、個々のフィールドを設定するのはかなり苦痛になります。クラスに別の非プリミティブクラスメンバーが定義されている場合もあります。次に、そのクラスのオブジェクトを作成し、個々の必須フィールドを設定すると、定型文を作成するためだけに開発作業がさらに増加します。
解決策は、上記のクラスのJSONスキーマを生成し、対応するデータをJSONファイルに1回追加することです。 Class1
を作成するテストクラスになります。オブジェクトの場合、オブジェクトを手動で作成する必要はありません。代わりに、JSONファイルを読み取り、ObjectMapper
を使用して、必要なClass1
にマップします。クラス:
ObjectMapper objectMapper = new ObjectMapper(); Class1 object = objectMapper.readValue( new String(Files.readAllBytes( Paths.get('src/test/resources/'+fileName)) ), Class1.class );
これは、JSONファイルを作成してそれに値を追加する1回限りの作業です。その後の新しいテストでは、新しいテストのニーズに応じてフィールドが変更されたJSONファイルのコピーを使用できます。
Beanの注入方法に応じて、Javaユニットテストを作成する方法がたくさんあることは明らかです。残念ながら、このトピックに関するほとんどの記事は1つの方法しかないと想定している傾向があるため、特に別の想定で記述されたコードを操作する場合は、混乱しがちです。うまくいけば、ここでのアプローチにより、開発者がモックを作成する正しい方法と、使用するテストランナーを見つける時間を節約できます。
使用する言語やフレームワークに関係なく(おそらくSpringやJUnitの新しいバージョンでも)、概念のベースは上記のJUnitチュートリアルで説明したものと同じです。ハッピーテスト!
JUnitは、Javaで単体テストを作成するための最も有名なフレームワークです。テストする実際のメソッドを呼び出すテストメソッドを作成します。テストケースは、渡されたパラメーターを指定して、期待値に対して戻り値をアサートすることにより、コードの動作を検証します。
Java開発者の大多数は、JUnitが最良のユニットテストフレームワークであることに同意しています。これは1997年以来デファクトスタンダードであり、他のJavaユニットテストフレームワークと比較して確かに最大のサポート量を持っています。
ユニットテストでは、個々のユニット(多くの場合、オブジェクトメソッドは「ユニット」と見なされます)が自動化された方法でテストされます。
JUnitテストは、作成したクラス内のメソッドの動作をテストするために使用されます。メソッドが期待どおりの結果をテストし、場合によっては例外がスローされるケースをテストします。メソッドが希望どおりに例外を処理できるかどうかを確認します。
JUnitは、単体テストを簡単に作成するためのさまざまなクラスとメソッドを提供するフレームワークです。
はい、JUnitはオープンソースプロジェクトであり、多くのアクティブな開発者によって維持されています。
JUnitは、開発者が単体テストを作成するときに使用する必要のある定型文を減らします。
KentBeckとErichGammaは最初にJUnitを作成しました。現在、オープンソースプロジェクトには100人以上の貢献者がいます。