简体   繁体   中英

How to test business logic (Doman Model Rules) while using ADO.net entity framework?

I am trying to test that the business rule of not allowing to share the same space with a user twice. Below is the method being tested. The line having the issue is marked below.

public void ShareSpace(string spaceToShare,string emailToShareIt)
{
  SharedSpace shareSpace = new SharedSpace();
  shareSpace.InvitationCode = Guid.NewGuid().ToString("N");
  shareSpace.DateSharedStarted = DateTime.Now;
  shareSpace.Expiration = DateTime.Now.AddYears(DefaultShareExpirationInYears);
  shareSpace.Active = true;
  shareSpace.SpaceName = spaceToShare;
  shareSpace.EmailAddress = emailToShareIt;
  if (!this.MySpacesShared.IsLoaded) 
     this.MySpacesShared.Load(); //Here I am getting the exception further below.

  if (this.MySpacesShared.Any(s => (s.EmailAddress == emailToShareIt) 
                              & (s.SpaceName == spaceToShare)))
    throw new InvalidOperationException("Cannot share the a space with a user twice.");
  else
    this.MySpacesShared.Add(shareSpace);
}

The TestMethod below:

[TestMethod]
public void Cannot_Share_SameSpace_with_same_userEmail_Twice()
{
    account.ShareSpace("spaceName", "user1@domain.com");
    try
    {
          account.ShareSpace("spaceName", "user1@domain.com");
          Assert.Fail("Should throw exception when same space is shared with same user.");
    }
    catch (InvalidOperationException)
    { /* Expected */ }
    Assert.AreEqual(1, account.MySpacesShared.Count);
    Assert.AreSame(null, account.MySpacesShared.First().InvitedUser);
}

The error that I am getting on the test results:

Test method SpaceHelper.Tests.Controllers.SpaceControllerTest.Cannot_Share_SameSpace_with_same_userEmail_Twice threw exception: System.InvalidOperationException: The EntityCollection could not be loaded because it is not attached to an ObjectContext..

When I go step by step on the debugging mechanism this error comes up on the Load() event. I am pretty sure it has to do with the fact that I don't have a ADO.NET Entity Framework on my test scenario since I am using fake information here and is not hooked to my database.

I case anyone wants to see here is my initialization for that test:

[TestInitialize()]
 public void MyTestInitialize() 
 {
     user = new User()
     {
         Active = true,
         Name = "Main User",
         UserID = 1,
         EmailAddress = "user1@userdomain.com",
         OpenID = Guid.NewGuid().ToString()
     };

     account = new Account()
     {
         Key1 = "test1",
         Key2 = "test2",
         AccountName = "Brief Account Description",
         ID = 1,
         Owner = user
     };
 }

From my personal experience, LINQ to SQL sucks big time when it comes to writing unit tests.

You BL layer is very coupled to LINQ to SQL classes in that it knows such concepts as .IsLoaded , can .Load() collections, etc. Move this logic to ISharedSpacesPersistenceService and rewrite your method as follows:

// Dependency-Inject this
public ISharedSpacesPersistenceService SharedSpacesPersistenceService { get; set; }

public void ShareSpace(string spaceToShare,string emailToShareIt)
{
    SharedSpace shareSpace = new SharedSpace();
    shareSpace.InvitationCode = Guid.NewGuid().ToString("N");
    shareSpace.DateSharedStarted = DateTime.Now;
    shareSpace.Expiration = DateTime.Now.AddYears(DefaultShareExpirationInYears);
    shareSpace.Active = true;
    shareSpace.SpaceName = spaceToShare;
    shareSpace.EmailAddress = emailToShareIt;

    if(SharedSpacesPersistenceService.ContainsSpace(s.EmailAddress, spaceToShare)
        throw new InvalidOperationException("Cannot share the a space with a user twice.");     

    this.MySpacesShared.Add(shareSpace);
}

And just a nitpick: replace DateTime.Now.AddYears(DefaultShareExpirationInYears) with DateTime.Now.Add(DefaultShareExpiration) and set DefaultShareExpiration type to TimeSpan . This will be much better.

The better approach is that you shouldn't touch the classes generated by Entity framework in your Domain layer. Instead, you should create your own business layer, and use LINQ to project the generated classes to your business object.

That way, you can set up your test in easier way.

I haven't used the Entity Framework, and I'm not 100% sure I know the full extent of what is happening here, but what you do need to do, is put a wrapper around your entity framework code that has an interface, and then use a mocking framework to pretend you are calling the database when you in fact are not. I'll give you the general idea, but you'll have to apply it to the Entity Framework, since I don't know the specifics.

public interface IShareSpaceGateway {
  IEnumerable<ShareSpace> GetSharedSpaces(string spaceToShare, string emailToShareIt);
}

public class ShareSpaceGatewayEF: IShareSpaceGateway
{
  // MySpacesShared should be included up here, not sure what type it is
  public IEnumerable<ShareSpace> GetSharedSpaces(string spaceToShare, string emailToShareIt)
  {
    if (!this.MySpacesShared.IsLoaded) 
     this.MySpacesShared.Load();

    return this.MySpacesShared.Any(s => (s.EmailAddress == emailToShareIt) 
                              & (s.SpaceName == spaceToShare));
  }
}

You could put whatever method you wanted in your ISharedSpaceGateway that makes sense. The idea is to reduce code repetition.

Now you want to be able to inject your new dependency on an IShareSpaceGateway. The best way to work with dependency injection is to use a DI Container like Castle Windsor, Structure Map, Ninject or Unity. I'm hypothesizing what your code might look like here:

public class Account
{
  private ISharedSpaceGateway _sharedSpaceGateway;
  public Account(ISharedSpaceGateway sharedSpaceGateway)
  {
    _sharedSpaceGateway = sharedSpaceGateway;
  }

  public int ID { get; set; }
  public string Key1 { get; set; }
  public string Key2 { get; set; }
  public string AccountName { get; set; }

  public void ShareSpace(string spaceToShare,string emailToShareIt)
  {
    SharedSpace shareSpace = new SharedSpace();
    shareSpace.InvitationCode = Guid.NewGuid().ToString("N");
    shareSpace.DateSharedStarted = DateTime.Now;
    shareSpace.Expiration = DateTime.Now.AddYears(DefaultShareExpirationInYears);
    shareSpace.Active = true;
    shareSpace.SpaceName = spaceToShare;
    shareSpace.EmailAddress = emailToShareIt;
    var sharedSpaces = sharedSpaceGateway.GetSharedSpaces(spaceToShare, emailToShareIt);
    if(sharedSpaces.Count() > 0)    
      throw new InvalidOperationException("Cannot share the a space with a user twice.");

    this.MySpacesShared.Add(shareSpace);
  }
}

Now, in your unit test, you want to use a mocking framework like Moq, or RhinoMocks to setup your test. In your test you don't want to use your real implementation of the SharedSpaceGateway, you want to pass in a fake one. This example uses RhinoMocks

public class TestFixture{


private ISharedSpaceGateway gateway;
[TestInitialize()]
 public void MyTestInitialize() 
 {
    gateway = MockRepository.CreateMock<ISharedSpaceGateway>();
    gateway.Expect(g => g.GetSharedSpaces("spaceName", "user1@domain.com"))
          .Return(new SharedSpace()); // whatever you want to return from the fake call

         user = new User()
         {
                 Active = true,
                 Name = "Main User",
                 UserID = 1,
                 EmailAddress = "user1@userdomain.com",
                 OpenID = Guid.NewGuid().ToString()
         };

         account = new Account(gateway) // inject the fake object
         {
                 Key1 = "test1",
                 Key2 = "test2",
                 AccountName = "Brief Account Description",
                 ID = 1,
                 Owner = user
         };
 }

[TestMethod]
public void Cannot_Share_SameSpace_with_same_userEmail_Twice()
{
        account.ShareSpace("spaceName", "user1@domain.com");
        try
        {
          account.ShareSpace("spaceName", "user1@domain.com");
          Assert.Fail("Should throw exception when same space is shared with same user.");
        }
        catch (InvalidOperationException)
        { /* Expected */ }
        Assert.AreEqual(1, account.MySpacesShared.Count);
        Assert.AreSame(null, account.MySpacesShared.First().InvitedUser);
        gateway.VerifyAllExpectations();
}

There is a lot involved in using DI frameworks and mocking frameworks, but these concepts make your code much more testable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM