brudert
brudert

Reputation: 537

Repository Pattern and Unit Testing ASP.NET Web API

I am just starting to dive into unit testing and am just starting to grasp the repository pattern and IoC. I don't think I fully understand it, however, because parts of it seems a bit silly. Let me explain.

My Controller:

public class UserProfileController : ApiController
{
    private IUserProfileRepository repository;

    // Optional constructor, passes repository, allows dependency injection
    public UserProfileController(IUserProfileRepository userProfileRepository)
    {
        this.repository = userProfileRepository;
    }

    // GET api/UserProfile
    // Returns a list of all users
    public IEnumerable<UserProfile> Get()
    {
        // Only Admins can see a list of users
        if (Roles.IsUserInRole("Admin"))
        {
            return repository.Get();
        }
        else
        {
            throw new HttpResponseException(
                new HttpResponseMessage(HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Administrator access required"
                });
        }
    }

// Other methods, etc.

(note that I have a dependency, Roles.IsUserInRole("Admin") that I can't figure out how to abstract, which causes some problems).

My typical repo interface:

public interface IUserProfileRepository : IDisposable
{
    IEnumerable<UserProfile> Get();
    // Other methods, etc.
}

Repo:

public class UserProfileRepository : IUserProfileRepository, IDisposable
{
    private OfootContext context;

    public UserProfileRepository(OfootContext context)
    {
        this.context = context;
    }

    public IEnumerable<UserProfile> Get()
    {
        return context.UserProfiles.AsEnumerable();
    }

// ... More code

So everything seems fine, I've abstracted my business access layer from my business logic and now I can create a fake repository to run unit tests.

Fake Repo:

public class FakeUserProfileRepository : IUserProfileRepository, IDisposable
{
    private List<UserProfile> context;

    public FakeUserProfileRepository(List<UserProfile> context)
    {
        this.context = context;
    }

    public IEnumerable<UserProfile> Get()
    {
        return context.AsEnumerable();
    }

and Test:

[TestMethod]
public void GetUsers()
{
    // Arrange
    var items = new List<UserProfile>()
    {
        new UserProfile
        {
            UserId = 1,
            Username = "Bob",
        },
        new UserProfile
        {
            UserId = 2,
            Username = "Bob2",
        }
    };

    FakeUserProfileRepository repo = new FakeUserProfileRepository(
        items);
    UserProfileController controller = new UserProfileController(
        repo);

    // Act
    IEnumerable<UserProfile> result = controller.Get();

    // Assert
    Assert.IsNotNull(result);
}

Now that we're on the same page (and feel free to point out any 'code smells'), here are my thoughts:

  1. The fake repository requires that I re-implement all of my Entity Framework logic and change it to dealing with a List object. This is more work and more links in the chain that I will need to debug.
  2. If the unit test does pass, it doesn't say anything about my code that accesses EF, so my application could still fail. This just means I need to test my EF code separately and have it hit a database.
  3. From #1, if the unit test is not testing the EF code, then it's just dealing with my authentication, authorization, and User creation code in my controller. Which it can't, because the WebSecurity and Roles classes hit my database.
  4. If I'm going to use a database to test EF code (point #2) and need a database to test the controller code (for authentication and authorization, point #3) then why bother even abstracting with a repository. Why don't I just abstract the context (using IContext or something?) and wire in a test database that is populated using the DropCreateDatabaseAlways class?

If I do figure out a way to abstract the user account garbage out, I am still just shuffling around code and creating more code (maybe even twice as much? Since I need to create fakes) where I could just replace the Context.

My question is: What am I missing? Is it an overall concept or is it something specific?

Upvotes: 8

Views: 7558

Answers (1)

hoserdude
hoserdude

Reputation: 829

You're on the right track. It's always painful getting things up and running, but you'll find it pays off down the road.

Rather than create "fake" objects, I recommend a framework like Moq. It allows you to set up the behavior you need at the time of the test, rather than re-implementing whole interfaces. For example, in your test you could simply write:

    Mock<IUserProfileRepository> mockUserRepo = new Mock<IUserProfileRepository>();
    var items = new List<UserProfile>()
    {
        new UserProfile
        {
            UserId = 1,
            Username = "Bob",
        },
        new UserProfile
        {
            UserId = 2,
            Username = "Bob2",
        }
    };
   mockUserRepo.Setup(m => m.Get().Returns(items.AsEnumerable());
   UserProfileController controller = new UserProfileController(
        mockUserRepo.Object);

    // Act
   IEnumerable<UserProfile> result = controller.Get();
   //Now you can keep varying the mock response by changing the Setup(), so now 
   //check for null response handling, 0 items, exceptions etc...

The net result of all this effort is that you've totally isolated the testing to your Controller, there are no DB dependencies and you can easily vary the inputs without writing classes, but rather playing with the mock setup.

If you follow this simple architectural pattern, you gain awesome testability and a clear separation of concerns. As things get more complex in your system, you can take advantage of a DI container like Unity.

On the authentication piece, I recommend creating Attributes you can decorate your methods with, like ASP.Net MVC uses: [Authorization(Roles="Admin")] as an example. This creates another useful cross-cutting pattern that keeps the Auth stuff decoupled from the business logic in the controller.

Upvotes: 13

Related Questions