I'm still trying to learn how to write good tests (and write testable code).
This is the function I want to verify,
void IAutoTisDal.UpdateRange(Range range)
{
using (var repo = _repositoryFactory.GetRepository(_baseline))
{
var rangeqry = from r in repo.Query<Range>()
where r.Id == range.Id
select r;
var rangeToUpdate = rangeqry.SingleOrDefault();
if (rangeToUpdate == null)
throw new Exception(string.Format("Range ID {0} not found", range.Id));
rangeToUpdate.Status = range.Status;
rangeToUpdate.FirstPsn = range.FirstPsn;
rangeToUpdate.LastPsn = range.LastPsn;
rangeToUpdate.PageCount = (int)(range.LastPsn - range.FirstPsn);
rangeToUpdate.CourierId = range.CourierId;
rangeToUpdate.WorkflowServer = range.WorkflowServer;
rangeToUpdate.DoubleFeedInd = range.DoubleFeedInd;
repo.Commit();
}
}
And the unit test:
[TestMethod]
public void UpdateRange_RangesPropertiesAreCopied()
{
var rangeToUpdate = new Range()
{
Id = 1, CourierId = 0, Status = 20,
WorkflowServer = null,
FirstPsn = -1, LastPsn = -1, PageCount = 0
};
var rangeToSave = new Range()
{
Id = 1, CourierId = 1234, Status = 40,
WorkflowServer = new PhysicalStation(){ Id = 1, Name = "fakeserver"},
FirstPsn = 123, LastPsn = 456, PageCount = 333
};
var repoMock = new Mock<IRepository>();
repoMock.Setup(x => x.Query<Range>()).Returns(new List<Range>(new[] {rangeToUpdate}).AsQueryable()).Verifiable();
repoMock.Setup(x => x.Commit()).Verifiable();
var factoryMock = new Mock<IRepositoryFactory>();
factoryMock.Setup(x => x.GetRepository(It.IsAny<string>())).Returns(repoMock.Object);
IAutoTisDal dal = new AutoTisDal(factoryMock.Object, "");
dal.UpdateRange(rangeToSave);
repoMock.VerifyAll();
Assert.AreEqual(rangeToUpdate.Id, rangeToSave.Id);
Assert.AreEqual(rangeToUpdate.CourierId, rangeToSave.CourierId);
Assert.AreEqual(rangeToUpdate.LastPsn, rangeToSave.LastPsn);
Assert.AreEqual(rangeToUpdate.FirstPsn, rangeToSave.FirstPsn);
Assert.AreEqual(rangeToUpdate.PageCount, rangeToSave.LastPsn - rangeToSave.FirstPsn);
Assert.AreSame(rangeToUpdate.WorkflowServer, rangeToSave.WorkflowServer);
Assert.AreEqual(rangeToUpdate.Status, rangeToSave.Status);
}
I know I need to write another test against the expected exception if no RangeId matches.
I also realize I'm writing test after the code.
2 Answers 2
First of all, your single method is doing multiple things:
- Get Range by id
- Copy properties from one instance to another
- Commit
Let's try to separate responsibilities:
Range GetRangeById(Repository repo, int id)
{
return repo.Query<Range>().SingleOrDefault(r=>r.Id == id);
}
void UpdateRange(Range rangeToUpdate, Range range)
{
// I'd even make this Range extension method
rangeToUpdate.Status = range.Status;
rangeToUpdate.FirstPsn = range.FirstPsn;
rangeToUpdate.LastPsn = range.LastPsn;
rangeToUpdate.PageCount = (int)(range.LastPsn - range.FirstPsn);
rangeToUpdate.CourierId = range.CourierId;
rangeToUpdate.WorkflowServer = range.WorkflowServer;
rangeToUpdate.DoubleFeedInd = range.DoubleFeedInd;
}
void IAutoTisDal.UpdateRange(Range range)
{
using (var repo = _repositoryFactory.GetRepository(_baseline))
{
var rangeToUpdate = GetRangeById(range.Id);
if (rangeToUpdate == null)
throw new Exception(string.Format("Range ID {0} not found", range.Id));
UpdateRange(rangeToUpdate, range);
repo.Commit();
}
}
Two new methods are already easier to understand and test. And, honestly, I don't think that writing test for IAutoTisDal.UpdateRange makes much sense. It won't provide much value.
-
\$\begingroup\$ Thanks for the input; It does make it a bit easier on the eyes. But why wouldn't the test provide much value? I added the test after realizing I forgot to set the
DoubleFeedInd
property (Range
class has a couple dozen properties). Perhaps I could have written the test to include only my missing property. \$\endgroup\$hometoast– hometoast2012年11月13日 14:57:51 +00:00Commented Nov 13, 2012 at 14:57 -
1\$\begingroup\$ I mean that after proposed refactoring this last method becomes quite simple. As I understand, tests provide value in (1) verify that newly written code works and (2) verify that it did not break after changes. In our case, last method is straightforward and relies on other (tested) methods. What can be verified there? That is throws an exception, or really updates db? \$\endgroup\$Pavel Tupitsyn– Pavel Tupitsyn2012年11月13日 15:09:25 +00:00Commented Nov 13, 2012 at 15:09
-
1\$\begingroup\$ Regarding forgotten property: your unit test won't detect the same bug when someone adds one more property to the Range entity. I think some metadata (reflection or data model or whatever - I'm not familiar with NHibernate, but I solved similar problem with Entity Framework) should be involved to verify that right properties are being copied (all public writeable except identity, probably). \$\endgroup\$Pavel Tupitsyn– Pavel Tupitsyn2012年11月13日 15:14:56 +00:00Commented Nov 13, 2012 at 15:14
-
\$\begingroup\$ I agree about the way to verify all properties are mapped/writable. In this case, the value of the other properties aren't important. \$\endgroup\$hometoast– hometoast2012年11月13日 15:46:31 +00:00Commented Nov 13, 2012 at 15:46
I dunno if I would use reflection for the range mapping to properties. I think this would be a great instance for Func<>
lambda expressions. Or perhaps a dictionary key value implementation that can be iterated through..
-
\$\begingroup\$ Can you please give an example how lambdas or dictionary can help to retrieve all db-mapped properties? \$\endgroup\$Pavel Tupitsyn– Pavel Tupitsyn2012年11月14日 06:15:12 +00:00Commented Nov 14, 2012 at 6:15
IRepository
. The concreteIRepository
uses NHibernate to exposesQuery<T>()
andCommit()
. \$\endgroup\$