ユニットテストを使用することが品質アーキテクチャへの大きな投資である理由

英語圏の1つの.netコミュニティでのユニットテストに特化したトピックは、ユニットテストがコードのリグレッションに対抗するツールであるだけでなく、高品質のアーキテクチャへの優れた投資であるという事実を理解するよう促しました。 このトピックの著者はジョニーであり、彼は金融業界の企業向けソフトウェアの開発に携わった会社での最初の(そして最後の)日について説明しました。 ジョニーは単体テスト開発者としての仕事を主張し、テストで担当したコードの質の低さに怒っていました。 彼は、自分が見たコードを、これに適さない場所でお互いを制御不能に作成するオブジェクトで満たされたダンプと比較しました。 彼はまた、リポジトリで抽象的なデータ型を見つけることができなかったと書きました。コードは、相互にクロスコールする密接に織り込まれた実装のみで構成されていました。 ジョニーは、この会社で単体テストの実践を適用することの無益さを理解し、彼を雇ったマネージャーに状況を説明し、さらなる協力を拒否し、彼の観点から最終的に貴重なアドバイスを与えました。 彼は、オブジェクトを正しくインスタンス化し、抽象データ型を利用することを教えることができるコースに開発チームを送ることを勧めました。 マネージャーがアドバイスに従ったかどうかはわかりません(私はそうは思いません)が、ジョニーが何を念頭に置いていたのか、ユニットテストの実践がアーキテクチャの品質にどのように影響するか興味があるなら、カットしてください、私たちは一緒に対処します。



依存関係の分離-単体テストの基礎


ユニットまたはユニットテストは、モジュールの機能を依存関係から切り離してチェックするテストです。 依存関係の分離とは、テスト対象のモジュールがプロトタイプの正しい動作をシミュレートするスタブとやり取りする実際のオブジェクトの置換を指します。 このような置換により、特定のモジュールのテストに集中でき、その環境の不正な動作の可能性を無視できます。 興味深い特性は、テストのフレームワーク内で依存関係を置き換える必要から生じます。 コードが単体テストでも使用されることを理解している開発者は、抽象性を最大限に活用し、高接続性の出現の最初の兆候でリファクタリングを行うことを余儀なくされます。



わかりやすくするための例


ジョニーが脱走した会社が開発したシステムで、プライベートメッセージを送信するためのモジュールがどのように見えるか想像してみましょう。 開発者が単体テストを適用した場合、同じモジュールはどのようになりますか。 モジュールはデータベースにメッセージを保存でき、メッセージの宛先のユーザーがシステム内にいる場合は、ポップアップ通知でメッセージを画面に表示します。



//     C#.  1. public class MessagingService { public void SendMessage(Guid messageAuthorId, Guid messageRecieverId, string message) { //        new MessagesRepository().SaveMessage(messageAuthorId, messageRecieverId, message); //,     if (UsersService.IsUserOnline(messageRecieverId)) { //  ,     NotificationsService.SendNotificationToUser(messageAuthorId, messageRecieverId, message); } } }
      
      







モジュールの依存関係を見てみましょう。 SendMessage関数は、NotificationsService、UsersServiceオブジェクトの静的メソッドを呼び出し、データベースの操作を担当するMessagesRepositoryオブジェクトを作成します。 モジュールが他のオブジェクトと相互作用しても問題はありません。 問題は、この相互作用がどのように構築されるかですが、うまく構築されません。 サードパーティのオブジェクトのメソッドへの直接アクセスにより、モジュールは特定の実装と密接に接続されました。 このやり取りには多くの欠点がありますが、主なことは、MessagingServiceモジュールがNotificationsService、UsersService、およびMessagesRepositoryオブジェクトの実装から分離してテストする機能を失ったことです。 単体テストの一部として、これらのオブジェクトをスタブに置き換えることはできません。

次に、開発者がテスト容易性を考慮した場合に、このモジュールがどのように見えるかを見てみましょう。



 //     C#.  2. public class MessagingService: IMessagingService { private readonly IUserService _userService; private readonly INotificationService _notificationService; private readonly IMessagesRepository _messagesRepository; public MessagingService(IUserService userService, INotificationService notificationService, IMessagesRepository messagesRepository) { _userService = userService; _notificationService = notificationService; _messagesRepository = messagesRepository; } public void AddMessage(Guid messageAuthorId, Guid messageRecieverId, string message) { //        _messagesRepository.SaveMessage(messageAuthorId, messageRecieverId, message); //,     if (_userService.IsUserOnline(messageRecieverId)) { //   _notificationService.SendNotificationToUser(messageAuthorId, messageRecieverId, message); } } }
      
      







このバージョンはすでにはるかに優れています。 現在、オブジェクト間の相互作用は直接ではなく、インターフェースを介して構築されています。 静的クラスを参照せず、ビジネスロジックを持つメソッドでオブジェクトをインスタンス化しません。 そして、最も重要なことは、テスト用のスタブをコンストラクターに渡すことで、すべての依存関係を置き換えることができることです。 したがって、コードのテスト容易性を達成することで、アプリケーションのアーキテクチャを同時に改善することができました。 インターフェイスを優先して実装の直接使用を放棄する必要があり、インスタンス化をより高いレベルのレイヤーに移動しました。 それがまさにジョニーが望んだことです。



メッセージ送信モジュールのテストを書いています


試験仕様
テストで正確にチェックするものを決定します。



  • IMessageRepository.SaveMessageメソッドへの単一の呼び出しの事実
  • IUsersServiceオブジェクト上のIsUserOnline()メソッドスタブがtrueを返した場合、INotificationsService.SendNotificationToUser()メソッドを1回呼び出したという事実
  • IUsersServiceオブジェクトのIsUserOnline()メソッドスタブがfalseを返した場合、INotificationsService.SendNotificationToUser()メソッドを呼び出さないこと


これらの3つの条件を満たせば、SendMessageメソッドの実装が正しく、エラーがないことが保証されます。



テスト
テストはMoq分離フレームワークを使用して実装されます。

 [TestMethod] public void AddMessage_MessageAdded_SavedOnce() { //Arrange // Guid messageAuthorId = Guid.NewGuid(); //,   Guid recieverId = Guid.NewGuid(); //,     string msg = "message"; //    IsUserOnline  IUserService Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior()); userServiceStub.Setup(x => x.IsUserOnline(It.IsAny<Guid>())).Returns(true); //  INotificationService  IMessagesRepository Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>(); Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>(); //  ,         var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object, repositoryMoq.Object); //Act messagingService.AddMessage(messageAuthorId, recieverId, msg); //Assert repositoryMoq.Verify(x => x.SaveMessage(messageAuthorId, recieverId, msg), Times.Once); } [TestMethod] public void AddMessage_MessageSendedToOffnlineUser_NotificationDoesntRecieved() { //Arrange // Guid messageAuthorId = Guid.NewGuid(); //   Guid offlineReciever = Guid.NewGuid(); //,     string msg = "message"; //    IsUserOnline  IUserService Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior()); userServiceStub.Setup(x => x.IsUserOnline(offlineReciever)).Returns(false); //  INotificationService  IMessagesRepository Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>(); Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>(); //  ,         var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object, repositoryMoq.Object); //Act messagingService.AddMessage(messageAuthorId, offlineReciever, msg); //Assert notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, offlineReciever, msg), Times.Never); } [TestMethod] public void AddMessage_MessageSendedToOnlineUser_NotificationRecieved() { //Arrange // Guid messageAuthorId = Guid.NewGuid(); //,   Guid onlineRecieverId = Guid.NewGuid(); //,     string msg = "message"; //    IsUserOnline  IUserService Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior()); userServiceStub.Setup(x => x.IsUserOnline(onlineRecieverId)).Returns(true); //  INotificationService  IMessagesRepository Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>(); Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>(); //  ,         var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object, repositoryMoq.Object); //Act messagingService.AddMessage(messageAuthorId, onlineRecieverId, msg); //Assert notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, onlineRecieverId, msg), Times.Once); }
      
      









完璧なアーキテクチャを見つける-無駄な練習


ユニットテストは、モジュール間の接続性が低い場合のアーキテクチャの優れたテストです。 ただし、複雑な技術システムの設計は常に妥協点であることに注意してください。 理想的なアーキテクチャはありません;設計中にアプリケーション開発のすべての可能なシナリオを考慮することは不可能です。 アーキテクチャの品質は多くのパラメーターに依存し、多くの場合、相互に排他的です。 設計レベルの問題は、抽象化レベルが多すぎるという問題を除き、追加の抽象化レベルを導入することで解決できます。 オブジェクト間の相互作用は抽象化に基づいてのみ構築されるべきであるという教義として考える必要はありません。主なことは、あなたが行う選択は意識的であり、実装間の相互作用を可能にするコードは柔軟性が低下し、その結果、能力を失うことを理解することですテスト済みの単体テスト。




All Articles