Monday, 12 August 2019

c# - How do you create a proper unit test for a method that returns a list?



I have this method:



public DataSourceResult GetProjectBySpec(int projectId, int seasonId, int episodeId)
{
using (var rep = RepositoryHelper.GetTake2Repository())
{
var spec = new ProjectCrewsByProjectSpec(projectId, seasonId, episodeId);

var personList = rep.GetList(spec).Select(p => new
{
//big query...
.ToDataSourceResult();

return personList;
}
}


I need to create a unit test for this.



My first question is:




  1. What am I testing for? Am I ONLY testing to see if the method returns a list?


  2. If so, how would I go about testing it?




This is what I have so far:



    [TestClass]
public class CrewControllerTest
{
[TestMethod]
public void GetProjectCrewsBySpecTest()
{
// arrange
int projectId = 1;
int seasonId = 2;
int episodeId = 3;

// act
var crewController = new CrewController();
DataSourceResult dsr = crewController.GetProjectCrewsBySpec(1, 2, 3);

// assert
// what or how do I assert here? Am I just checking whether "dsr" is a list? How do I do that?
}
}

Answer



I'm no expert either,and I've only been doing TDD for a small while so take what I write in this rambling answer with a spoonful of salt :) I am sure someone else can point out if I've made any really bad mistakes or pointed you in the wrong direction...



I'm not sure your test is really a Unit Test because it's exercising multiple dependencies. Suppose for a moment that you run this test and get an exception thrown out of the method.
Did this exception come from




  • RepositoryHelper.GetTake2Repository())
    throwing? (Depencency issue)

  • ProjectCrewsByProjectSpec constructor throwing? (Dependency issue)

  • rep.GetList(spec) throwing? the kendo (it is
    kendo,right?) (Dependency issue)

  • ToDataSourceResult() throwing? (Behavioural issue)



Unit testing is all about testing things in complete isolation from their dependencies, so at the moment I'd say it's more like an integration test whereby you don't really care how the systems interact, you just want to make sure that for a given projectID, seasonId and episodeId you get back the expected results - in this case what are really testing is the rep.GetList() method in conjunction with the .ToDataSourceResult extension.



Now integration tests are very very useful and 100% required as part of a test driven methodology,and if that's what you really want to do, then you're doing it about right.(I put this in and expect that back; did I get that back?)



But if you want to Unit Test this code (specifically, if you want to unit test your classes' GetProjectBySpec method) you will have to do as @jimmy_keen mentioned and refactor it so that you can test the behaviour of GetProjectBySpec.
e.g. here is a specified behaviour I just invented, of course yours might be different:




  • If input is bad, throw ArgumentException

  • Creates a new ProjectCrewsByProjectSpec object

  • Calls rep.GetList and passes spec to it

  • Returns a non-null DataSourceResult



The first thing you need to do in order to be able to test that GetProjectBySpec does all of the things in the above list is to refactor it so that it doesn't create its own dependencies - instead, you give it the dependencies it needs, via Dependency Injection.



DI really works best when you are injecting by Interface, so in whatever class provides this method, your constructor for that class should take an instance of for example IRepositoryHelper and store it in a private readonly member. It should also take an instance of IProjectCrewsByProjectSpecFactory which you'd use to create your spec. Now since you want to test what GetProjectBySpec actually does with these dependencies then you'll be using a mocking framework such as Moq which I won't go into here except in the example below.



If neither of these classes currently implement such an interface, then just use Visual Studio to extract the interface definition for you based on the class definition. If they are 3rd party classes over which you have no control, this might be tricky.



But let's assume for a minute that you can define interfaces thus: (bear with me on the generic <> bits which I'm never 100% on, I am sure someone smarter than me can tell you where all the "T"'s should go...) Code below is not tested nor checked for typos!



public interface IRepositoryHelper
{
IList GetList(IProjectCrewsByProjectSpecFactory spec);
}

public interface IProjectCrewsByProjectSpecFactory
{
ProjectDGACrew Create(int projectId, int seasonId, int episodeId);
}


Your code would then end up looking something like this:



//somewhere in your class definition
private readonly IRepositoryHelper repo;
private readonly IProjectCrewsByProjectSpecFactory pfactory;
//constructor
public MyClass(IRepositoryHelper repo, IProjectCrewsByProjectSpecFactory pfactory)
{
this.repo = repo;
this.pfactory=pfactory;
}
//method to be tested
public DataSourceResult GetProjectBySpec(int projectId, int seasonId, int episodeId)
{
var spec = pfactory.Create(projectId, seasonId, episodeId);
var personList = repo.GetList(spec).Select(p => new
{//big query...}).ToDataSourceResult();
return personList;
}


now you have 4 test methods to write:



[TestMethod]
[ExepctedException(typeof(ArgumentException)]
public void SUT_WhenInputIsBad_ThrowsArgumentException()
{
var sut = new MyClass(null,null); //don't care about our dependencies for this check
sut.GetProjectBySpec(0,0,0); //or whatever is invalid input for you.
//don't care about the return, only that the method throws.
}



[TestMethod]
public void SUT_WhenInputIsGood_CreatesProjectCrewsByProjectSpec()
{
//create dependencies using Moq framework.
var pf= new Mock();
var repo = new Mock>();
//setup such that a call to pfactory.Create in the tested method will return nothing
//because we actually don't care about the result - only that the Create method is called.
pf.Setup(p=>p.Create(1,2,3)).Returns(new ProjectDgaCrew());
//setup the repo such that any call to GetList with any ProjectDgaCrew object returns an empty list
//again we do not care about the result.
//This mock dependency is only being used here
//to stop an exception being thrown from the test method
//you might want to refactor your behaviours
//to specify an early exit from the function when the factory returns a null object for example.
repo.Setup(r=>r.GetList(It.IsAny()).Returns>(new List());
//create our System under test, inject our mock objects:
var sut = new MyClass(repo,pf.Object);
//call the method:
sut.GetProjectBySpec(1,2,3);
//and verify that it did indeed call the factory.Create method.
pf.Verify(p=>p.Create(1,2,3),"pf.Create was not called with 1,2,3");
}
public void SUT_WhenInputIsGood_CallsRepoGetList(){} //you get the idea
public void SUT_WhenInputIsGood_ReturnsNonNullDataSourceResult(){}//and so on.


Hope that gives you some help...and of course you can refactor your test classes to avoid a lot of the mocking setup and have it all in a single place to keep the lines of code to a minimum.


No comments:

Post a Comment

php - file_get_contents shows unexpected output while reading a file

I want to output an inline jpg image as a base64 encoded string, however when I do this : $contents = file_get_contents($filename); print &q...