views:

213

answers:

3

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", "[email protected]");
 try
 {
          account.ShareSpace("spaceName", "[email protected]");
          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 = "[email protected]",
   OpenID = Guid.NewGuid().ToString()
  };

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

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.

Anton Gogolev
Hi Anton: Could you give me the reasoning around using TimeSpan instead of adding a year to today's date. I tried googling but only found definitions. Thanks.
Geo
A: 

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.

J.W.
A: 

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", "[email protected]"))
          .Return(new SharedSpace()); // whatever you want to return from the fake call

         user = new User()
         {
                 Active = true,
                 Name = "Main User",
                 UserID = 1,
                 EmailAddress = "[email protected]",
                 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", "[email protected]");
        try
        {
          account.ShareSpace("spaceName", "[email protected]");
          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.

NerdFury
Hi NerdFury: I found my problem. As others mentioned I have my automatically created entity model classes mixed with my business logic. The main reason is lack of proper knowledge on ADO.NET EN FW. I followed your code and was able to test it using an interface of the Entity class on the account class. Is kind of ugly. Any advice on how to fix this flaw in my design without refactoring the entire application. Thanks again, and I marked your question as the answer.
Geo
The basic concept is to find your applications boundarys or seams. That is anyplace you have a dependency on an external source. It could be the file system, it could be a web service, or the database. In order to unit test your business logic, you have to have a way to avoid those dependencies, which is waht I showed above. If you can't refactor completely, then try to find places where you are using code to cross boundaries, and try to encapsulate that code in a class that you can mock the behavior of.
NerdFury