神刀安全网

SUT Double

While it’s possible to rely on Extract and Override for unit testing, Dependency Injection can make code and tests simpler and easier to maintain.

In object-oriented programming, many people still struggle with the intersection of design and testability. If a unit test is an automated test of a unit in isolation of its dependencies , then how do you isolate an object from its dependencies, most notably databases, web services, and the like?

One technique, described in The Art of Unit Testing , is called Extract and Override . The idea is that you write a class, but use a Template Method , Factory Method , or other sort of virtual class method to expose extensibility points that a unit test can use to achieve the desired isolation.

People sometimes ask me whether that isn’t good enough, and why I advocate the (ostensibly) more complex technique of Dependency Injection ?

It’s easiest to understand the advantages and disadvantages if we have an example to discuss.

Example: Extract and Override

Imagine that you’re creating a reservation system for a restaurant. Clients (web sites, smart phone apps, etcetera) display a user interface where you can fill in details about your reservation: your name, email address, the number of guests, and the date of your reservation. When you submit your reservation request, the client POSTs a JSON document to a web service.

In this example, the web service is implemented by a Controller class, and the Post method handles the incoming request:

public int Capacity { get; }   public IHttpActionResult Post(ReservationDto reservationDto) {     DateTime requestedDate;     if(!DateTime.TryParse(reservationDto.Date, out requestedDate))         return this.BadRequest("Invalid date.");       var reservedSeats = this.ReadReservedSeats(requestedDate);     if(this.Capacity < reservationDto.Quantity + reservedSeats)         return this.StatusCode(HttpStatusCode.Forbidden);       this.SaveReservation(requestedDate, reservationDto);       return this.Ok(); }

The implementation is simple: It first attempts to validate the incoming document, and returns an error message if the document is invalid. Second, it reads the number of already reserved seats via a helper method, and rejects the request if the remaining capacity is insufficient. On the other hand, if the remaining capacity is sufficient, it saves the reservation and returns 200 OK.

The Post method relies on two helper methods that handles communication with the database:

public virtual int ReadReservedSeats(DateTime date) {     const string sql = @"         SELECT COALESCE(SUM([Quantity]), 0) FROM [dbo].[Reservations]         WHERE YEAR([Date]) = YEAR(@Date)         AND MONTH([Date]) = MONTH(@Date)         AND DAY([Date]) = DAY(@Date)";       var connStr = ConfigurationManager.ConnectionStrings["booking"]         .ConnectionString;       using (var conn = new SqlConnection(connStr))     using (var cmd = new SqlCommand(sql, conn))     {         cmd.Parameters.Add(new SqlParameter("@Date", date));           conn.Open();         return (int)cmd.ExecuteScalar();     } }   public virtual void SaveReservation(     DateTime dateTime,     ReservationDto reservationDto) {     const string sql = @"         INSERT INTO [dbo].[Reservations] ([Date], [Name], [Email], [Quantity])         VALUES (@Date, @Name, @Email, @Quantity)";       var connStr = ConfigurationManager.ConnectionStrings["booking"]         .ConnectionString;       using (var conn = new SqlConnection(connStr))     using (var cmd = new SqlCommand(sql, conn))     {         cmd.Parameters.Add(             new SqlParameter("@Date", reservationDto.Date));         cmd.Parameters.Add(             new SqlParameter("@Name", reservationDto.Name));         cmd.Parameters.Add(             new SqlParameter("@Email", reservationDto.Email));         cmd.Parameters.Add(             new SqlParameter("@Quantity", reservationDto.Quantity));           conn.Open();         cmd.ExecuteNonQuery();     } }

In this example, both helper methods are public , but they could have been protected without changing any conclusions; by being public , though, they’re easier to override using Moq . The important detail is that both of these methods are overridable. In C#, you declare that with the virtual keyword; in Java, methods are overridable by default.

Both of these methods use elemental ADO.NET to communicate with the database. You can also use an ORM, or any other database access technique you prefer – it doesn’t matter for this discussion. What matters is that the methods can be overridden by unit tests.

Here’s a unit test of the happy path:

[Fact] public void PostReturnsCorrectResultAndHasCorrectStateOnAcceptableRequest() {     var json =             new ReservationDto             {                 Date = "2016-05-31",                 Name = "Mark Seemann",                 Email = "mark@example.com",                 Quantity = 1             };     var sut = new Mock<ReservationsController> { CallBase = true };     sut         .Setup(s => s.ReadReservedSeats(new DateTime(2016, 5, 31)))         .Returns(0);     sut         .Setup(s => s.SaveReservation(new DateTime(2016, 5, 31), json))         .Verifiable();                  var actual = sut.Object.Post(json);       Assert.IsAssignableFrom<OkResult>(actual);     sut.Verify(); }

This example uses the Extract and Override technique, but instead of creating a test-specific class that derives from ReservationsController, it uses Moq to create a dynamic Test Double for the System Under Test ( SUT ) – a SUT Double.

Since both ReadReservedSeats and SaveReservation are virtual, Moq can override them with test-specific behaviour. In this test, it defines the behaviour of ReadReservedSeats in such a way that it returns 0 when the input is a particular date.

While the test is simple, it has a single blemish. It ought to follow the Arrange Act Assert pattern, so it shouldn’t have to configure the SaveReservation method before it calls the the Post method. After all, the SaveReservation method is a Command, and you should useMocks for Commands. In other words, the test ought to verify the interaction with the SaveReservation method in the Assert phase; not configure it in the Arrange phase.

Unfortunately, if you don’t configure the SaveReservation method before calling Post , Moq will use the base implementation, which will attempt to interact with the database. The database isn’t available in the unit test context, so without that override, the base method will throw an exception, causing the test to fail.

Still, that’s a minor issue. In general, the test is easy to follow, and the design of the ReservationsController class itself is also straightforward. Are there any downsides?

Example: shared connection

From a design perspective, the above version is fine, but you may find it inefficient that ReadReservedSeats and SaveReservation both open and close a connection to the database. Wouldn’t it be more efficient if they could share a single connection?

If ( by measuring ) you decide that you’d like to refactor the ReservationsController class to use a shared connection, your first attempt might look like this:

public IHttpActionResult Post(ReservationDto reservationDto) {     DateTime requestedDate;     if (!DateTime.TryParse(reservationDto.Date, out requestedDate))         return this.BadRequest("Invalid date.");       using (var conn = this.OpenDbConnection())     {         var reservedSeats = this.ReadReservedSeats(conn, requestedDate);         if (this.Capacity < reservationDto.Quantity + reservedSeats)             return this.StatusCode(HttpStatusCode.Forbidden);           this.SaveReservation(conn, requestedDate, reservationDto);           return this.Ok();     } }   public virtual SqlConnection OpenDbConnection() {     var connStr = ConfigurationManager.ConnectionStrings["booking"]         .ConnectionString;       var conn = new SqlConnection(connStr);     try     {         conn.Open();     }     catch     {         conn.Dispose();         throw;     }     return conn; }   public virtual int ReadReservedSeats(SqlConnection conn, DateTime date) {     const string sql = @"         SELECT COALESCE(SUM([Quantity]), 0) FROM [dbo].[Reservations]         WHERE YEAR([Date]) = YEAR(@Date)         AND MONTH([Date]) = MONTH(@Date)         AND DAY([Date]) = DAY(@Date)";       using (var cmd = new SqlCommand(sql, conn))     {         cmd.Parameters.Add(new SqlParameter("@Date", date));           return (int)cmd.ExecuteScalar();     } }   public virtual void SaveReservation(     SqlConnection conn,     DateTime dateTime,     ReservationDto reservationDto) {     const string sql = @"         INSERT INTO [dbo].[Reservations] ([Date], [Name], [Email], [Quantity])         VALUES (@Date, @Name, @Email, @Quantity)";       using (var cmd = new SqlCommand(sql, conn))     {         cmd.Parameters.Add(             new SqlParameter("@Date", reservationDto.Date));         cmd.Parameters.Add(             new SqlParameter("@Name", reservationDto.Name));         cmd.Parameters.Add(             new SqlParameter("@Email", reservationDto.Email));         cmd.Parameters.Add(             new SqlParameter("@Quantity", reservationDto.Quantity));           cmd.ExecuteNonQuery();     } }

You’ve changed both ReadReservedSeats and SaveReservation to take an extra parameter: the connection to the database. That connection is created by the OpenDbConnection method, but you also have to make that method overridable, because otherwise, it’d attempt to connect to a database during unit testing, and thereby causing the tests to fail.

You can still unit test using the Extract and Overide technique, but the test becomes more complicated:

[Fact] public void PostReturnsCorrectResultAndHasCorrectStateOnAcceptableRequest() {     var json =             new ReservationDto             {                 Date = "2016-05-31",                 Name = "Mark Seemann",                 Email = "mark@example.com",                 Quantity = 1             };     var sut = new Mock<ReservationsController> { CallBase = true };     sut.Setup(s => s.OpenDbConnection()).Returns((SqlConnection)null);     sut         .Setup(s => s.ReadReservedSeats(             It.IsAny<SqlConnection>(),             new DateTime(2016, 5, 31)))         .Returns(0);     sut         .Setup(s => s.SaveReservation(             It.IsAny<SqlConnection>(),             new DateTime(2016, 5, 31), json))         .Verifiable();                  var actual = sut.Object.Post(json);       Assert.IsAssignableFrom<OkResult>(actual);     sut.Verify(); }

Not only must you override ReadReservedSeats and SaveReservation , but you must also supply a Dummy Object for the connection object, as well as override OpenDbConnection . Still manageable, perhaps, but the design indisputably deteriorated.

You can summarise the flaw by a single design smell: Feature Envy . Both the ReadReservedSeats and the SaveReservation methods take an argument of the type SqlConnection. On the other hand, they don’t use any instance members of the ReservationsController class that currently hosts them. These methods seem like they ought to belong to SqlConnection instead of ReservationsController. That’s not possible, however, since SqlConnection is a class from the Base Class Library, but you can, instead, create a new Repository class.

Example: Repository

A common design pattern is the Repository pattern, although the way it’s commonly implemented today has diverged somewhat from the original description in PoEAA . Here, I’m going to apply it like people often do. You start by defining a new class:

public class SqlReservationsRepository : IDisposable {     private readonly Lazy<SqlConnection> lazyConn;       public SqlReservationsRepository()     {         this.lazyConn = new Lazy<SqlConnection>(this.OpenSqlConnection);     }       private SqlConnection OpenSqlConnection()     {         var connStr = ConfigurationManager.ConnectionStrings["booking"]             .ConnectionString;           var conn = new SqlConnection(connStr);         try         {             conn.Open();         }         catch         {             conn.Dispose();             throw;         }         return conn;     }       public virtual int ReadReservedSeats(DateTime date)     {         const string sql = @"             SELECT COALESCE(SUM([Quantity]), 0) FROM [dbo].[Reservations]             WHERE YEAR([Date]) = YEAR(@Date)             AND MONTH([Date]) = MONTH(@Date)             AND DAY([Date]) = DAY(@Date)";           using (var cmd = new SqlCommand(sql, this.lazyConn.Value))         {             cmd.Parameters.Add(new SqlParameter("@Date", date));               return (int)cmd.ExecuteScalar();         }     }       public virtual void SaveReservation(         DateTime dateTime,         ReservationDto reservationDto)     {         const string sql = @"             INSERT INTO [dbo].[Reservations] ([Date], [Name], [Email], [Quantity])             VALUES (@Date, @Name, @Email, @Quantity)";           using (var cmd = new SqlCommand(sql, this.lazyConn.Value))         {             cmd.Parameters.Add(                 new SqlParameter("@Date", reservationDto.Date));             cmd.Parameters.Add(                 new SqlParameter("@Name", reservationDto.Name));             cmd.Parameters.Add(                 new SqlParameter("@Email", reservationDto.Email));             cmd.Parameters.Add(                 new SqlParameter("@Quantity", reservationDto.Quantity));               cmd.ExecuteNonQuery();         }     }       public void Dispose()     {         this.Dispose(true);         GC.SuppressFinalize(this);     }       protected virtual void Dispose(bool disposing)     {         if (disposing)             this.lazyConn.Value.Dispose();     } }

The new SqlReservationsRepository class contains the two ReadReservedSeats and SaveReservation methods, and because you’ve now moved them to a class that contains a shared database connection, the methods don’t need the connection as a parameter.

The ReservationsController can use the new SqlReservationsRepository class to do its work, while keeping connection management efficient. In order to make it testable , however, you must make it overridable. The SqlReservationsRepository class’ methods are already virtual, but that’s not the class you’re testing. The System Under Test is the ReservationsController class, and you have to make its use of SqlReservationsRepository overridable as well.

If you wish to avoid Dependency Injection, you can use a Factory Method :

public IHttpActionResult Post(ReservationDto reservationDto) {     DateTime requestedDate;     if (!DateTime.TryParse(reservationDto.Date, out requestedDate))         return this.BadRequest("Invalid date.");       using (var repo = this.CreateRepository())     {         var reservedSeats = repo.ReadReservedSeats(requestedDate);         if (this.Capacity < reservationDto.Quantity + reservedSeats)             return this.StatusCode(HttpStatusCode.Forbidden);           repo.SaveReservation(requestedDate, reservationDto);           return this.Ok();     } }          public virtual SqlReservationsRepository CreateRepository() {     return new SqlReservationsRepository(); }

The Factory Method in the above example is the CreateRepository method, which is virtual, and thereby overridable. You can override it in a unit test like this:

[Fact] public void PostReturnsCorrectResultAndHasCorrectStateOnAcceptableRequest() {     var json =             new ReservationDto             {                 Date = "2016-05-31",                 Name = "Mark Seemann",                 Email = "mark@example.com",                 Quantity = 1             };     var repo = new Mock<SqlReservationsRepository>();     repo         .Setup(r => r.ReadReservedSeats(new DateTime(2016, 5, 31)))         .Returns(0);     repo         .Setup(r => r.SaveReservation(new DateTime(2016, 5, 31), json))         .Verifiable();     var sut = new Mock<ReservationsController> { CallBase = true };     sut.Setup(s => s.CreateRepository()).Returns(repo.Object);       var actual = sut.Object.Post(json);       Assert.IsAssignableFrom<OkResult>(actual);     repo.Verify(); }

You’ll notice that not only did the complexity increase of the System Under Test, but the test itself became more complicated as well. In the previous version, at least you only needed to create a single Mock<T> , but now you have to create two different test doubles and connect them. This is a typical example demonstrating the shortcomings of the Extract and Override technique. It doesn’t scale well as complexity increases.

Example: Dependency Injection

In 1994 we we were taught to favor object composition over class inheritance . You can do that, and still keep your code loosely coupled with Dependency Injection . Instead of relying on virtual methods, inject a polymorphic object into the class:

public class ReservationsController : ApiController {     public ReservationsController(IReservationsRepository repository)     {         if (repository == null)             throw new ArgumentNullException(nameof(repository));           this.Capacity = 12;         this.Repository = repository;     }       public int Capacity { get; }       public IReservationsRepository Repository { get; }       public IHttpActionResult Post(ReservationDto reservationDto)     {         DateTime requestedDate;         if (!DateTime.TryParse(reservationDto.Date, out requestedDate))             return this.BadRequest("Invalid date.");           var reservedSeats =             this.Repository.ReadReservedSeats(requestedDate);         if (this.Capacity < reservationDto.Quantity + reservedSeats)             return this.StatusCode(HttpStatusCode.Forbidden);           this.Repository.SaveReservation(requestedDate, reservationDto);           return this.Ok();     } }

In this version of ReservationsController, an IReservationsRepository object is injected into the object via the constructor and saved in a class field for later use. When the Post method executes, it calls Repository.ReadReservedSeats and Repository.SaveReservation without further ado.

The IReservationsRepository interface is defined like this:

public interface IReservationsRepository {     int ReadReservedSeats(DateTime date);     void SaveReservation(DateTime dateTime, ReservationDto reservationDto); }

Perhaps you’re surprised to see that it merely defines the two ReadReservedSeats and SaveReservation methods, but makes no attempt at making the interface disposable.

Not only is IDisposable an implementation detail, but it also keeps the implementation of ReservationsController simple. Notice how it doesn’t attempt to control the lifetime of the injected repository, which may or may not be disposable. In a few paragraphs, we’ll return to this matter, but first, witness how the unit test became simpler as well:

[Fact] public void PostReturnsCorrectResultAndHasCorrectStateOnAcceptableRequest() {     var json =             new ReservationDto             {                 Date = "2016-05-31",                 Name = "Mark Seemann",                 Email = "mark@example.com",                 Quantity = 1             };     var repo = new Mock<IReservationsRepository>();     repo         .Setup(r => r.ReadReservedSeats(new DateTime(2016, 5, 31)))         .Returns(0);     var sut = new ReservationsController(repo.Object);       var actual = sut.Post(json);       Assert.IsAssignableFrom<OkResult>(actual);     repo.Verify(         r => r.SaveReservation(new DateTime(2016, 5, 31), json)); }

With this test, you can finally use the Arrange Act Assert structure, instead of having to configure the SaveReservation method call in the Arrange phase. This test arranges the Test Fixture by creating a Test Double for the IReservationsRepository interface and injecting it into the ReservationsController. You only need to configure the ReadReservedSeats method, because there’s no default behaviour that you need to suppress.

You may be wondering about the potential memory leak when the SqlReservationsRepository is in use. After all, ReservationsController doesn’t dispose of the injected repository.

You address this concern when you compose the dependency graph. This example uses ASP.NET Web API, which has an extensibility point for this exact purpose :

public class PureCompositionRoot : IHttpControllerActivator {     public IHttpController Create(         HttpRequestMessage request,         HttpControllerDescriptor controllerDescriptor,         Type controllerType)     {         if(controllerType == typeof(ReservationsController))         {             var repo = new SqlReservationsRepository();             request.RegisterForDispose(repo);             return new ReservationsController(repo);         }           throw new ArgumentException(             "Unexpected controller type: " + controllerType,             nameof(controllerType));     } }

The call to request.RegisterForDispose tells the ASP.NET Web API framework to dispose of the concrete repo object once it has handled the request and served the response.

Conclusion

At first glance, it seems like the overall design would be easier if you make your SUT testable via inheritance, but once things become more complex, favouring composition over inheritance gives you more freedom to design according to well-known design patterns.

In case you want to dive into the details of the code presented here, it’s available on GitHub . You can follow the progression of the code in the repository’s commit history.

If you’re interested in learning more about advanced unit testing techniques, you can watch my popular Pluralsight course .

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » SUT Double

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址