単体テストがコードのリグレッションに取り組むためのツールであるだけでなく、高品質のアーキテクチャへの多大な投資でもあることを示すために、この記事を書くことにしました。さらに、英語の.NETコミュニティのトピックが、私にこれを行う動機を与えました。記事の著者はジョニーでした。彼は、金融セクターのビジネス向けソフトウェア開発に携わった会社での最初と最後の日について説明しました。ジョニーは、ユニットテストの開発者のポジションに応募していました。彼は、テストしなければならなかったコード品質の低さに腹を立てました。彼は、コードを、不適切な場所で互いに複製するオブジェクトが詰め込まれた廃品置き場と比較しました。さらに、彼はリポジトリ内で抽象データ型を見つけることができませんでした。コードには、相互に相互要求する実装のバインディングのみが含まれていました。
ジョニーは、この会社でのモジュールテストの無用さをすべて認識し、この状況をマネージャーに説明し、それ以上の協力を拒否し、貴重なアドバイスを提供しました。彼は、開発チームがオブジェクトのインスタンス化と抽象データ型の使用を学ぶためのコースに進むことを推奨しました。マネージャーが彼のアドバイスに従ったかどうかはわかりません(彼は従わなかったと思います)。ただし、ジョニーの意味と、モジュールテストの使用がアーキテクチャの品質にどのように影響するかに興味がある場合は、この記事を読んでください。
依存関係の分離はモジュールテストのベースです
モジュールまたは単体テストは、依存関係から分離されたモジュール機能を検証するテストです。依存関係の分離は、テスト対象のモジュールが相互作用する実際のオブジェクトを、プロトタイプの正しい動作をシミュレートするスタブに置き換えることです。この置換により、特定のモジュールのテストに集中でき、その環境の誤った動作の可能性を無視できます。テストで依存関係を置き換える必要があると、興味深いプロパティが発生します。自分のコードがモジュールテストで使用されることに気付いた開発者は、抽象化を使用して開発し、高い接続性の最初の兆候でリファクタリングを実行する必要があります。
特定の例で検討します。
ジョニーが逃げ出した会社が開発したシステムで、パーソナルメッセージモジュールがどのように見えるか想像してみましょう。そして、開発者が単体テストを適用した場合、同じモジュールはどのように見えるか。
モジュールはメッセージをデータベースに保存できる必要があり、メッセージの宛先がシステムに含まれている場合は、トースト通知とともにメッセージを画面に表示します。
//A module for sending messages in C#. Version 1.
public class MessagingService
{
public void SendMessage(Guid messageAuthorId, Guid messageRecieverId, string message)
{
//A repository object stores a message in a database
new MessagesRepository().SaveMessage(messageAuthorId, messageRecieverId, message);
//check if the user is online
if (UsersService.IsUserOnline(messageRecieverId))
{
//send a toast notification calling the method of a static object
NotificationsService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
}
}
} モジュールの依存関係を確認しましょう。
SendMessage関数は、NotificationsserviceオブジェクトとUsersserviceオブジェクトの静的メソッドを呼び出し、データベースの操作を担当するMessagesrepositoryオブジェクトを作成します。
モジュールが他のオブジェクトと相互作用するという事実に問題はありません。問題は、このインタラクションがどのように構築されているかであり、正常に構築されていません。サードパーティのメソッドに直接アクセスできるため、モジュールは特定の実装に緊密にリンクされています。
この相互作用には多くの欠点がありますが、重要なことは、Messagingserviceモジュールが、Notificationsservice、Usersservice、およびMessagesrepositoryの実装から分離してテストする機能を失ったことです。実際、これらのオブジェクトをスタブに置き換えることはできません。
それでは、開発者が同じモジュールを処理した場合、同じモジュールがどのように見えるかを見てみましょう。
//A module for sending messages in C#. Version 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)
{
//A repository object stores a message in a database.
_messagesRepository.SaveMessage(messageAuthorId, messageRecieverId, message);
//check if the user is online
if (_userService.IsUserOnline(messageRecieverId))
{
//send a toast message
_notificationService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
}
}
}
ご覧のとおり、このバージョンの方がはるかに優れています。オブジェクト間の相互作用は、直接ではなく、インターフェースを介して構築されるようになりました。
静的クラスにアクセスしたり、ビジネスロジックを使用してメソッド内のオブジェクトをインスタンス化したりする必要はもうありません。重要な点は、テスト用のスタブをコンストラクターに渡すことで、すべての依存関係を置き換えることができるということです。したがって、コードの妥当性を向上させると同時に、コードの妥当性とアプリケーションのアーキテクチャの両方を向上させることもできます。実装を直接使用することを拒否し、インスタンス化を上のレイヤーに渡しました。これはまさにジョニーが望んでいたことです。
次に、メッセージ送信モジュールのテストを作成します。
テストの仕様
テストでチェックする内容を定義します:
- SaveMessageメソッドの1回の呼び出し
- IUsersServiceオブジェクトに対するIsUserOnline()メソッドスタブがtrueを返す場合のSendNotificationToUser()メソッドの1回の呼び出し
- IUsersServiceオブジェクトに対するIsUserOnline()メソッドスタブがfalseを返す場合、SendNotificationToUser()メソッドはありません
これらの条件に従うことで、SendMessageメッセージの実装が正しく、エラーが含まれていないことを保証できます。
テスト
テストは、分離されたMoqフレームワークを使用して実装されます
[TestMethod]
public void AddMessage_MessageAdded_SavedOnce()
{
//Arrange
//sender
Guid messageAuthorId = Guid.NewGuid();
//receiver who is online
Guid recieverId = Guid.NewGuid();
//a message sent from a sender to a receiver
string msg = "message";
// stub for the IsUserOnline interface of the IUserService method
Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
userServiceStub.Setup(x => x.IsUserOnline(It.IsAny<Guid>())).Returns(true);
//mocks for INotificationService and IMessagesRepository
Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
//create a module for messages passing mocks and stubs as dependencies
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
//sender
Guid messageAuthorId = Guid.NewGuid();
//receiver who is offline
Guid offlineReciever = Guid.NewGuid();
//message sent from a sender to a receiver
string msg = "message";
// stub for the IsUserOnline interface of the IUserService method
Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
userServiceStub.Setup(x => x.IsUserOnline(offlineReciever)).Returns(false);
//mocks for INotificationService and IMessagesRepository
Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
// create a module for messages passing mocks and stubs as dependencies
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
//sender
Guid messageAuthorId = Guid.NewGuid();
//receiver who is online
Guid onlineRecieverId = Guid.NewGuid();
//message sent from a sender to a receiver
string msg = "message";
// stub for the IsUserOnline interface of the IUserService method
Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
userServiceStub.Setup(x => x.IsUserOnline(onlineRecieverId)).Returns(true);
//mocks for INotificationService and IMessagesRepository
Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
//create a module for messages passing mocks and stubs as dependencies
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);
}をアサートします 要約すると、理想的なアーキテクチャを探すことは無意味な作業です。
ユニットテストは、モジュール間の結合が失われたときにアーキテクチャをチェックする必要がある場合に使用すると便利です。それでも、複雑なエンジニアリングシステムの設計は常に妥協点であることに注意してください。理想的なアーキテクチャはなく、アプリケーション開発のすべてのシナリオを事前に考慮することはできません。アーキテクチャの品質は複数のパラメータに依存し、多くの場合、相互に排他的です。抽象化のレベルを追加することで、設計上の問題を解決できます。ただし、それは大量の抽象化レベルの問題については言及していません。オブジェクト間の相互作用が抽象化のみに基づいていると考えることはお勧めしません。重要なのは、実装間の相互作用を可能にし、柔軟性が低いコードを使用することです。つまり、単体テストでテストする可能性はありません。