Friday, September 5, 2014

ASP.NET MVC Unit Testing Part 4: The Tests and What Was Learned

So, now that the project is refactored with interfaces, I have fake data, and a fake environment, it's time to run some unit tests! Whoohoo!  I'll go over some of the basic tests and also cover what I learned over all throughout the unit testing process.


Basic Unit Test


The basic pattern of a unit test, that is beat into my head in every tutorial and demo video that I see, is Arrange, Act, and Assert. The arrange step is where we inject all our of dependencies, passing our fakes off to the code under test (or "system under test" if you prefer). Then during the act step, we new up our controller (or model, or whatever is being tested really...). Finally, we make some assertions regarding the results of our action. If all is well we get that magical green checkmark and all is right in the world. Initially, most of my tests looked something like this:
        [TestMethod]
public void SchoolController_DeleteListItem_Fail_When_School_Not_Found()
{
    //Arrange
    var db = new FakeUnitOfWork();
    IPrincipal fakeUser = FakeUser.GetFakeUser(0);
    var controller = new SchoolController(dbfakeUser);
    db = TestData.addTestData(db);
 
    //Act
    var result = controller.DeleteListItem(99"Fail University"trueas JsonResult;
 
    //Assert
    Assert.IsNotNull(result);
    Assert.AreEqual("Could not look up school..."result.Data);
    Assert.AreEqual(0, ((FakeGenericRepository<LuSchool>)db.LuSchools).Removed.Count);
    Assert.AreEqual(false, ((FakeGenericRepository<LuSchool>)db.LuSchools).Saved);
}

While one common best practice I've read is that you should only have one assert, I feel, at least at this point in my programming life, that DRY trumps that rule of thumb, and if all the asserts I'm making relate to the same arrangement and action, then to me it makes sense to make them all in one place. One problem I did notice after writing about fifty of these guys is that I was going pretty much the exact same arragement for every single test. So I tried putting them in the contructor for the test class and voila, mass extinction of arrange code across all my unit tests. Now most of them look like this:

        [TestMethod]
public void UnitPersonnelController_EditList_Returns_Units()
{
    //Arrange
 
    //Act
    ViewResult result = controller.EditList() as ViewResult;
    int unitCount = Enumerable.Count(db.LuUnits);
 
    //Assert
    Assert.IsNotNull(result);
    var data = (UnitPersonnelModel)result.ViewData.Model;
    Assert.IsNotNull(data);
    Assert.AreEqual(unitCountdata.Units.Count());
}

While the top of the test class looks like so:

    [TestClass]
public class UnitPersonnelControllerTest
{
    private FakeUnitOfWork db;
    private IPrincipal fakeUser;
    private UnitPersonnelController controller;      
 
    public UnitPersonnelControllerTest()
    {
        try
        {
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
        catch
        {
            //gulp
        }
        
        db = new FakeUnitOfWork();
        fakeUser = FakeUser.GetFakeUser(0);
        controller = new UnitPersonnelController(dbfakeUser);
        db = TestData.addTestData(db);
        controller.ControllerContext = new FakeControllerContext();
 
        //THIS IS HOW I PROBABLY SHOULD HAVE HANDLED User INJECTION:
        controller.ControllerContext.HttpContext.User = fakeUser;
    }


I say "probably" on that last line because I didn't implement it this way, and I haven't tried it. So it might just blow up in which case doing it the way I've done it already would make sense.


Lessons Learned

Throughout this process I probably learned more about programming in ASP.NET than a class or book could have touched. There is definitely something to be said for just getting your hands dirty.


Testing anonymous Json objects with reflection


Remember that bit about hitting UrlHelper.Action()? Let me give you the full picture, first the test:

[TestMethod]
public void UnitPersonnelController_AddListItem_Redirects_After_Add()
{
    //Arrange
 
    //Act
    var result = controller.AddListItem(1"0");
    Type type = result.Data.GetType();
    bool propFound = false;
    string propValue = "";
    foreach (var item in type.GetProperties())
    {
        if (item.Name == "Url")
        {
            propFound = true;
            propValue = (string)item.GetValue(result.Datanull);
        }
    }
 
    //Assert
    Assert.IsNotNull(result);
    Assert.IsTrue(propFound);
    Assert.AreEqual("/UnitPersonnel/UnitFocus?unitId=1"propValue);
}



And here is the controller action being tested:
public JsonResult AddListItem(int Unit_Idstring AspNetUserID)
{
    if (ModelState.IsValid)
    {
        UnitPersonnel newPersonnel = new UnitPersonnel() { Unit_Id = Unit_IdAspNetUserID = AspNetUserIDInsertBy = _user.Identity.GetUserId(), InsertDate = DateTime.Now };
        db.UnitPersonnels.Insert(newPersonnel);
        db.Commit();
 
        var redirectUrl = new UrlHelper(Request.RequestContext).Action("UnitFocus""UnitPersonnel"new { unitId = Unit_Id });
        return Json(new { Url = redirectUrl });
    }
    return Json("Failed to Add - Controller");
}

It turned out that getting the HttpContext faked out to the point that UrlHelper would work was only half the battle. I wanted to check that the returned Json object had the correct url, but since it was an anonymous type I really struggled with how to pick it apart. After a frustrated afternoon I finally left (late) without a resolution, but on the way home I remembered seeing mention of this concept called "reflection" on forums and such, and a lightbulb went off. I looked it up as soon as I got home, and armed with the near certainty of success the next morning was able to churn out the above code within five minutes of logging on. Oh that sweet smell of success. I ended up recycling this code on several other test methods.


LINQ to Objects != LINQ to Entities


I saw this objection several times when I was trying to create a fake for my DbContext. Basically, there are enough differences between the two providers that sometimes queries that will work fine with one will not work with the other, and certain features aren't available for both.

Here is an example where LINQ to Entities loaded up all the related UnitPersonnel:

But LINQ to Objects didn't when I ran the unit test:

The only object in UnitPersonnels was the one the method added, even though there were related UnitPersonnel in the test data. As a result, my test just notes whether a single UserPersonnel exists on the Unit:
        [TestMethod]
public void UnitPersonnelController_EditListAdd_Returns_Units_Plus_New_UnitPersonnel()
{
    //Arrange
 
    //Act
    ViewResult result = controller.EditListAdd(1as ViewResult;
    int unitCount = Enumerable.Count(db.LuUnits);
    //int upCount = Enumerable.Count(db.UnitPersonnels.Where(up => up.Unit_Id == 1)) + 1; //eager loading doesn't work with linq-to-objects
 
    //Assert
    Assert.IsNotNull(result);
    var data = (UnitPersonnelModel)result.ViewData.Model;
    Assert.IsNotNull(data);
    Assert.AreEqual(1data.Units.Where(u => u.Unit_Id == 1).SingleOrDefault().UnitPersonnels.Count());
    Assert.AreEqual(0data.Units.Where(u => u.Unit_Id == 2).SingleOrDefault().UnitPersonnels.Count()); //only specified unit has new unitpersonnel
    Assert.AreEqual(unitCountdata.Units.Count());
}


Turns out there was really no way to get around this issue. Using .Include() doesn't help because it doesn't do anything with LINQ to Objects. It MAY have been possible to refactor the query... maybe something like chained selects that grab the related UnitPersonnel and assign them to the collection... but refactoring JUST to make the test work seemed excessive.

One last oddity I ran into working with LINQ to Objects was the treatment of Nulls in a left outer join. I was getting a NullReference Exception with the following piece of code:

var results = (from Term in _ctx.Terms.Where(o => o.Is_Visible == true)
               join Application in _ctx.Applications.Where(e => e.AspNetUsersID == aspNetUserId && e.Application_Id != application_Id && e.School_Id == school_Id)
                   on Term.Term_Id equals Application.Term_Id into ljoin
               from lj in ljoin.DefaultIfEmpty()
               where lj.Application_Id == null
               select Term);


This was working with the live database, so I couldn't figure out why in the hell it wasn't working in the unit test. After monkeying around with the query to get a better idea of what the internals look like, come to find out that the culprit was this line:

where lj.Application_Id == null


The idea behind this query (which took me forever to figure out it felt like) was that only the terms that this user hasn't used for this same school should be available to select. So you join the ones that have been used and return the terms with a NULL value in the join. Works fine in SQL, but what happens here is that it isn't just lj.Application_Id that is NULL, it's the whole entry:

The applications that joined show up in lj as valid applications, but the other four are null. This is what causes the above where clause to throw the NullRef exception: null doesn't have a Application_Id. The solution was to just add a null check before it started looking at properties:
where (lj == null || lj.Application_Id == null)

Now everyone is happy.

One final observation about the limitation of using fakes in place of Entity Framework is that Update behavior is practically impossible to emulate. My fakes use a real simple mechanism that really only checks that the Update, Delete, Insert, and Commit methods have been called on the expected number of objects, but with Update, I can't confirm that the correct changes were made. It is really easy to make a test of Update pass for code that won't update anything or throw an exception when run against LINQ to Entities. 

For one, it seems that Update, at least the way I've implemented it here, must be run at a specific point in the change process. For reasons that are still a mystery to me, if you modify the entity, THEN run Update, it doesn't save the changes. But if you run Update before making the changes, they save just fine. I also found myself running into issues of Duplicate Key exceptions. Basically, Entity Framework thought it was already tracking my object, so I couldn't update it. I think this was less a LINQ issue and more of a "lack of understanding about EF" issue. When this exception reared it's head I generally just had to be careful about what I was pulling down from the context.


Stored Procedure Headaches


I ran into a unique issue in the later stages of the project. There is a search function that returns a list of applications based on given criteria. Results are also based on the active user's role (An admin can see everybody, a "unituser" can see apps in the same unit). This was being done through a stored procedure, which made testing much more difficult. While working on everything else, I gave just this method a new DbContext:
public JsonResult SearchJson(EAApplicationSearchDataTablePageRequest dataTablePageRequest)
        {
            EducationAssistanceEntities db = new EducationAssistanceEntities();
            var results = (from r in db.usp_ApplicationSearch(
                             dataTablePageRequest.FirstName
                           , dataTablePageRequest.LastName
                           , dataTablePageRequest.LastFourSSN
                           , dataTablePageRequest.School_Id_Parm
                           , dataTablePageRequest.Unit_Id_Parm
                           , dataTablePageRequest.Verified
                           , dataTablePageRequest.Submitted
                           , dataTablePageRequest.AspNetUsersID
                                                             )
                           select new { r.Application_Idr.First_Namer.MIr.Last_Namer.Unitr.Schoolr.Degreer.Programr.Termr.Verifiedr.Submitted }).ToList();
 
 
            var output = new
            {
                sEcho = dataTablePageRequest.Echo,
                iTotalRecords = results.Count,
                iTotalDisplayRecords = results.Count,
                aaData = results
            };
 
            return Json(outputJsonRequestBehavior.AllowGet);
        }

This is the stored procedure called by db.usp_ApplicationSearch. The left join with UnitPersonnel is used to determine if a "unit user" is authorized to view the application. Because a given unit will have multiple UnitPersonnel, the left join creates duplicates of each application. ROW_NUMBER here is being used to deduplicate the application results:
select *
from
(
 select Application_Id
    ,First_Name
    ,MI, Last_Name
    ,b.Descr as Unit
    ,c.Descr as School
    ,d.Descr as Degree
    ,a.Program
    ,t.Descr as Term
    ,convert(bit,case when Verified_Date is null then 0 else 1 end) as Verified
    ,convert(bit,case when Submit_Date is null then 0 else 1 end) as Submitted
    ,ROW_NUMBER() over(partition by Application_Id order by Application_Id) as rn
 from Applications as a
 inner join LuUnits as b
  on a.Unit_Id=b.Unit_Id
 inner join LuSchools as c
  on a.School_Id=c.School_ID
 inner join Term as t
  on a.Term_Id = t.Term_Id
 inner join LuDegree as d
  on a.Degree_Id = d.Degree_Id
 left outer join UnitPersonnel as up
  on a.Unit_Id=up.Unit_Id
 where (First_Name like '%'+@fname+'%' or @fname is null)
 and (Last_name like '%'+@lname+'%' or @lname is null)
 and (Last_Four_SSN like '%'+@l4ssn+'%' or @l4ssn is null)
 and (a.School_Id = @schoolId or @schoolId is null)
 and (a.Unit_Id = @unitId or @unitId is null)
 and (case when Verified_Date is null then 0 else 1 end= @Verified or @Verified=3)
 and (case when Submit_Date is null then 0 else 1 end= @Submitted or @Submitted=3)
 and (up.AspNetUserID=@AspNetUsersID or @EaAdmin = 1)
) as data
where rn =1


So, I had to make a choice here. I could add a method to the Unit of Work, or create an ApplicationRepository interface and class with the same method. In either case, the SQL implementation of the interface would call the stored procedure and the fake would do... what exactly? Return some arbitrary data perhaps, but then if the whole point of the controller action is to return only certain results, it seems silly to fudge the test.

The other alternative was to eliminate the stored procedure all together in favor of LINQ. Despite the limitations and pitfalls of L2E vs L2O discussed above, it seems plausible that I could still write meaningful tests for a LINQ query. Besides the immediate testibility question, I did find an interesting article that laid out some pretty reasonable arguments (both in the body and in the comments) for getting away from sprocs. I was sold, I just had to figure out how to write the LINQ...

Most of the query is easily reproduced in LINQ, but I did run into obstacles. First I got hung up trying to reproduce ROW_NUMBER. While it can be done, it was adding a lot of (unnecessary) complexity to the query. So I stepped back and asked "What are we trying to accomplish here?" The row number wasn't needed per se, it was just a matter of eliminating duplicates. The most obvious solution was to tack on a .Distinct() to the end of the query:
var results = (
    from apps in
    (
         from r in db.Applications
         join u in db.LuUnits on r.Unit_Id equals u.Unit_Id
         join s in db.LuSchools on r.School_Id equals s.School_ID
         join dg in db.LuDegrees on r.Degree_Id equals dg.Degree_Id
         join t in db.Terms on r.Term_Id equals t.Term_Id
         join up in db.UnitPersonnels on r.Unit_Id equals up.Unit_Id into lj
         from unitp in lj.DefaultIfEmpty()
         where (userid == unitp.AspNetUserID || isAdmin&&
               (fn == null || r.First_Name.Contains(fn)) &&
               (ln == null || r.Last_Name.Contains(ln)) &&
               (lfs == null || r.Last_Four_SSN.Contains(lfs)) &&
               (!sid.HasValue || sid == (r.School_Id)) &&
               (!uid.HasValue || uid == (r.Unit_Id)) &&
               (ved == 3 || (ved == 1 && !(r.Verified_By == null)) 
                         || (ved == 0 && (r.Verified_By == null))) &&
               (sub == 3 || (sub == 1 && !(r.Submit_Date == null)) 
                         || (sub == 0 && (r.Submit_Date == null)))
               select new
               {
                   App = new {
                             Application_Id = r.Application_Id,
                             First_Name = r.First_Name,
                             MI = r.MI,
                             Last_Name = r.Last_Name,
                             Unit = u.Descr,
                             School = s.Descr,
                             Degree = dg.Descr,
                             Program = r.Program,
                             Term = t.Descr,
                             Verified = r.Verified_By == null ? 0 : 1,
                             Submitted = r.Submit_Date == null ? 0 : 1
                             }
               }
         )
         select apps.App
    ).Distinct().ToList();

There is kind of an unwritten rule against using Distinct around here, so I had a feeling it wouldn't really fly. I had to leave it for a while though, my brain was fried. Eventually, though, I did learn of other capabilities of LINQ and figured out that a simple filter would be an easy way of avoiding the use of Distinct:
if (isAdmin)
{
     authorizedUnits = from u in db.LuUnits
                       select u.Unit_Id;
}
else if (_user.IsInRole("EaUnitUser"))
{
     authorizedUnits = from up in db.UnitPersonnels
                       where ((userid == up.AspNetUserID))
                       select up.Unit_Id;
}          
 
var results = (
    from apps in
    (
         from r in db.Applications
         join u in db.LuUnits on r.Unit_Id equals u.Unit_Id
         join s in db.LuSchools on r.School_Id equals s.School_ID
         join dg in db.LuDegrees on r.Degree_Id equals dg.Degree_Id
         join t in db.Terms on r.Term_Id equals t.Term_Id
         where (authorizedUnits.Contains(u.Unit_Id)) &&
               (fn == null || r.First_Name.Contains(fn)) &&
               (ln == null || r.Last_Name.Contains(ln)) &&
               (lfs == null || r.Last_Four_SSN.Contains(lfs)) &&
               (!sid.HasValue || sid == (r.School_Id)) &&
               (!uid.HasValue || uid == (r.Unit_Id)) &&
               (ved == 3 || (ved == 1 && !(r.Verified_By == null)) 
                         || (ved == 0 && (r.Verified_By == null))) &&
               (sub == 3 || (sub == 1 && !(r.Submit_Date == null)) 
                         || (sub == 0 && (r.Submit_Date == null)))
               select new
               {
                    App = new {
                    Application_Id = r.Application_Id,
                    First_Name = r.First_Name,
                    MI = r.MI,
                    Last_Name = r.Last_Name,
                    Unit = u.Descr,
                    School = s.Descr,
                    Degree = dg.Descr,
                    Program = r.Program,
                    Term = t.Descr,
                    Verified = r.Verified_By == null ? 0 : 1,
                    Submitted = r.Submit_Date == null ? 0 : 1
                    }
               }
      )
      select apps.App
   ).ToList();


As it is now, doing it this way requires two queries, though I'm pretty sure it would be refactored to drop the query that picks authorizedUnits as a subquery directly into the main results query, but I haven't tried to tackle that yet. Guess the lesson here is there is always more than one way to skin the cat.


A Word on Microsoft Fakes


Savvy unit testers are probably reading this (assuming they haven't already checked out tl;dr style) and thinking "You know Troy, you'd have saved yourself a lot of trouble using [insert test/mock framework here]" That is probably true, but I have to understand the hard way before I start looking at stuff to make it easier, and overall it's been a good learning experience. I definitely know a lot more about what is going on behind the scenes in MVC. But during this unit testing adventure, I DID take a peek at Microsoft Fakes on the suggestion of a more experienced colleague.

I read a number of articles, but these three videos were probably the most informative for me. They weren't just about using Fakes but also really reinforced WHY I was unit testing at all:
The most consistent take away I got from these videos is this: shims bad, stubs good. Basically, if I'm programming to interfaces and my program is written in a way that is testible, I'll be able to use stubs. Basically all the fakes I wrote for my own unit tests are just stubs. Shims, on the other hand, intercept program execution at runtime and redifine the behavior there. Whereas a good unit test should be a black box test and allow for refactoring, shims require knowledge of the inner working of the code under test, and they are very sensitive to changes. For example, if I shim DateTime.Now in my test, and refactor my code to use DateTime.Today, the code will break, even though nothing changed semantically in the code.

Shims do have their purpose, however. Some code is very tightly coupled, and refactoring is difficult because there is fear that making changes will break functionality. One of the presenters offered this paradox: To refactor the code, it needs to be under unit tests, but to write unit tests, the code needs to be refactored. It's a catch 22. This is where using shims is appropriate: they allow you to write the unit tests without refactoring, so the behavior is pinned and there is a degree of safety, THEN refactoring can take place.

I'm sure my understanding of Microsoft Fakes and how to use them is naive at best, so I won't linger on the topic. But I do see a lot of potential.


No comments:

Post a Comment