TDD for Web API with NUnit, NSubtitute - c#

I'm still confused with some TDD concepts and how to do it correctly. I'm trying to use it implement for a new project using Web API. I have read a lot about it, and some article suggest NUnit as a testing framework and NSubstitute to mock the repository.
What I don't understand is with NSubstitute we can define the expected result of what we want, is this valid if we want to validate our code logic?
Let's say I have a controller like this with Put and Delete method:
[BasicAuthentication]
public class ClientsController : BaseController
{
// Dependency injection inputs new ClientsRepository
public ClientsController(IRepository<ContactIndex> clientRepo) : base(clientRepo) { }
[HttpPut]
public IHttpActionResult PutClient(string accountId, long clientId, [FromBody] ClientContent data, string userId = "", string deviceId = "", string deviceName = "")
{
var result = repository.UpdateItem(new CommonField()
{
AccountId = accountId,
DeviceId = deviceId,
DeviceName = deviceName,
UserId = userId
}, clientId, data);
if (result.Data == null)
{
return NotFound();
}
if (result.Data.Value != clientId)
{
return InternalServerError();
}
IResult<IDatabaseTable> updatedData = repository.GetItem(accountId, clientId);
if (updatedData.Error)
{
return InternalServerError();
}
return Ok(updatedData.Data);
}
[HttpDelete]
public IHttpActionResult DeleteClient(string accountId, long clientId, string userId = "", string deviceId = "")
{
var endResult = repository.DeleteItem(new CommonField()
{
AccountId = accountId,
DeviceId = deviceId,
DeviceName = string.Empty,
UserId = userId
}, clientId);
if (endResult.Error)
{
return InternalServerError();
}
if (endResult.Data <= 0)
{
return NotFound();
}
return Ok();
}
}
and I create some unit tests like this:
[TestFixture]
public class ClientsControllerTest
{
private ClientsController _baseController;
private IRepository<ContactIndex> clientsRepository;
private string accountId = "account_id";
private string userId = "user_id";
private long clientId = 123;
private CommonField commonField;
[SetUp]
public void SetUp()
{
clientsRepository = Substitute.For<IRepository<ContactIndex>>();
_baseController = new ClientsController(clientsRepository);
commonField = new CommonField()
{
AccountId = accountId,
DeviceId = string.Empty,
DeviceName = string.Empty,
UserId = userId
};
}
[Test]
public void PostClient_ContactNameNotExists_ReturnBadRequest()
{
// Arrange
var data = new ClientContent
{
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
clientsRepository.CreateItem(commonField, data)
.Returns(new Result<long>
{
Message = "Bad Request"
});
// Act
var result = _baseController.PostClient(accountId, data, userId);
// Asserts
Assert.IsInstanceOf<BadRequestErrorMessageResult>(result);
}
[Test]
public void PutClient_ClientNotExists_ReturnNotFound()
{
// Arrange
var data = new ClientContent
{
contactName = "TestContactName 1",
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
clientsRepository.UpdateItem(commonField, clientId, data)
.Returns(new Result<long?>
{
Message = "Data Not Found"
});
var result = _baseController.PutClient(accountId, clientId, data, userId);
Assert.IsInstanceOf<NotFoundResult>(result);
}
[Test]
public void PutClient_UpdateSucceed_ReturnOk()
{
// Arrange
var postedData = new ClientContent
{
contactName = "TestContactName 1",
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
var expectedResult = new ContactIndex() { id = 123 };
clientsRepository.UpdateItem(commonField, clientId, postedData)
.Returns(new Result<long?> (123)
{
Message = "Data Not Found"
});
clientsRepository.GetItem(accountId, clientId)
.Returns(new Result<ContactIndex>
(
expectedResult
));
// Act
var result = _baseController.PutClient(accountId, clientId, postedData, userId)
.ShouldBeOfType<OkNegotiatedContentResult<ContactIndex>>();
// Assert
result.Content.ShouldBe(expectedResult);
}
[Test]
public void DeleteClient_ClientNotExists_ReturnNotFound()
{
clientsRepository.Delete(accountId, userId, "", "", clientId)
.Returns(new Result<int>()
{
Message = ""
});
var result = _baseController.DeleteClient(accountId, clientId, userId);
Assert.IsInstanceOf<NotFoundResult>(result);
}
[Test]
public void DeleteClient_DeleteSucceed_ReturnOk()
{
clientsRepository.Delete(accountId, userId, "", "", clientId)
.Returns(new Result<int>(123)
{
Message = ""
});
var result = _baseController.DeleteClient(accountId, clientId, userId);
Assert.IsInstanceOf<OkResult>(result);
}
}
Looking at the code above, am I writing my unit tests correctly? I feel like I'm not sure how it will validate the logic in my controller.
Please ask for more information, if there is anything that needs clarified.

If the code you've actually posted is a true reflection of your test to code ratio, then you don't appear to be following a TDD approach. One of the core concepts is that you don't write code that hasn't been tested. This means that as a basic rule, you need to have a minimum of one test for every branch in your code, otherwise there would be no reason for the branch to have been written.
Looking at your DeleteClient method there are three branches, so there should be at least three tests for the method (you've only posted two).
// Test1 - If repo returns error, ensure expected return value
DeleteClient_Error_ReturnsInternalError
// Test2 - If repo returns negative data value, ensure expected return value
DeleteClient_NoData_ReturnsNotFound
// Test3 - If repo returns no error, ensure expected return
DeleteClient_Success_ReturnsOk
You can use NSubtitute to redirect your code down these different paths so that they can be tested. So, to redirect down the InternalError branch you would setup your substitute something like this:
clientsRepository.Delete(Args.Any<int>(), Args.Any<int>(),
Args.Any<string>(), Args.Any<string>(),
Args.Any<int>())
.Returns(new Result<int>()
{
Error = SomeError;
});
Without knowing the IRepository interface it's hard to be 100% accurate about the NSubstitute setup, but basically, the above is saying when the Delete method of the substitute is called with the given parameter types (int,int,string,string,int) the substitute should return a value which has Error set to SomeError (this is the trigger for the InternalError branch of logic). You would then assert that when calling the system under test, it returns InternalServerError.
You need to repeat this for each of your logic branches. Don't forget that you'll need to setup the substitute to return all appropriate values for to get to each branch of logic. So, to get to the ReturnsNotFound branch, you'd need to make your repository return NoError and a negative Data value.
I said above, you needed a minimum of one test for each branch of logic. It's a minimum because there are other things that you will want to test. In the above substitute setups, you'll notice that I'm using Args.Any<int> etc. That's because for the behaviour the tests above are interested in, it doesn't really matter if the correct values are being passed to the repository or not. Those tests are testing the logic flows influenced by the return values of the repository. For your testing to be complete, you'll also need to make sure that the correct values are being passed to the repository. Depending on your approach, you might have a test per parameter, or you might have a test to validate all of the parameters in the call to your repository.
To validate all of the parameters, taking the ReturnsInternalError test as a base, you would simply have to add a validation call to the subsistute something like this to validate the parameters:
clientsRepository.Received().Delete(accountId, userId, "", "", clientId);
I'm using the ReturnsInternalError test as a base because after validating the the call, I want to get out of the method under test as fast as possible and in this instance it's by returning an error.

First, when coding in TDD, you must make the smallest functions possible. About three lines of code (excluding brackets and signature) THe function should have only one purpose. Ex: a function called GetEncriptedData should call two other methods GetData and EncryptData instead of getting the data and encrypt it. If your tdd is well done, that shouldn't be a problem to get to that result. When functions are too long, tests are pointless as they can't really cover all your logic. And my tests Use the having when then logic. Ex.: HavingInitialSituationA_WhenDoingB_ThenShouldBecomeC is the name of the test.
You would find three blocks of code inside your test representing these three parts. There is more. When doing tdd, you shoud always do one step at once. If you expect your function to return 2, make a test that validates if it returns two, and make your function literally return 2. Afther that you could want some conditions and test them in other test cases and all your tests should bass at the end. TDD is a completely different way to code. YOu make one test, it fails, you make the necessary code so it pass, and you make another test, it fails... This is my experience, and my way of implementing TDD tells me that you are wrong. But this is my point of view. Hope I helped you.

Related

Why does SonarQube claim the `return` line is not covered by a unit test?

Here's my API endpoint:
[HttpPost]
public int Post(SearchHistory searchHistory)
{
IDashboardRepository dashboardrepos = new DashboardRepository();
int historyId = dashboardrepos.SaveSearchHistoryByUser(searchHistory);
return historyId;
}
And here is the SonarQube report:
The two green bars on lines 37 & 38 indicate that they are covered by the unit test. But for some reason like 39 isn't?
Here's the test:
[TestMethod()]
public void GlobalSeach_PutTest()
{
SearchHistory history = new SearchHistory {
// redacted for ease of reading on SO
}
var controller = new GlobalSearchController(_config);
int? response = controller.Post(history);
Assert.IsTrue(response != null);
}
Your Postmethod returns an int. On your test you are expecting to receive a nullable int (int?).
My gues is the problem is that your are not really testing the result of your method when you use this assert: Assert.IsTrue(response != null);. The first problem is that kind of test will never fail.
I imagine that your dashboardrepos.SaveSearchHistoryByUser method should return the primary key of the entity you just persist on your db. Based on that assumption I suggest you to refactoring your test as I describe below, to improve and solve the problem with coverage.
[TestMethod()]
public void GlobalSeach_PutTest()
{
SearchHistory history = new SearchHistory {
// redacted for ease of reading on SO
}
// _dashboardreposMock is an example of Mock<IDashboardRepository>
_dashboardreposMock.Setup(_ => _.SaveSearchHistoryByUser(It.IsAny<SearchHistory>)).Returns(1);
var controller = new GlobalSearchController(_config);
int response = controller.Post(history);
Assert.IsTrue(response == 1);
}

XUnit how to mock IMemoryCache ASP.NET Core

I understand IMemoryCache.Set is an extension method so it can not be mocked. People have provided workarounds to such situation e.g as one by the NKosi here. I am wondering how I can achieve that for my data access layer where my MemoryCache returns a value and when not found it gets data from the db, set it to the MemoryCache and return the required value.
public string GetMessage(int code)
{
if(myMemoryCache.Get("Key") != null)
{
var messages= myMemoryCache.Get<IEnumerable<MyModel>>("Key");
return messages.Where(x => x.Code == code).FirstOrDefault().Message;
}
using (var connection = dbFactory.CreateConnection())
{
var cacheOptions = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(1) };
const string sql = #"SELECT Code, Message FROM MyTable";
var keyPairValueData = connection.Query<KeyPairValueData>(sql);
myMemoryCache.Set("Key", keyPairValueData, cacheOptions );
return keyPairValueData.Where(x => x.Code == code).FirstOrDefault().Message;
}
}
Following is my Unit Test - And off course it is not working as I can't mock IMemoryCache
[Fact]
public void GetMessage_ReturnsString()
{
//Arrange
// Inserting some data here to the InMemoryDB
var memoryCacheMock = new Mock<IMemoryCache>();
//Act
var result = new DataService(dbConnectionFactoryMock.Object, memoryCacheMock.Object).GetMessage(1000);
//assert xunit
Assert.Equal("Some message", result);
}
The first thing I would say is why not use a real memory cache? It would verify the behavior much better and there's no need to mock it:
// Arrange
var memCache = new MemoryCache("name", new NameValueCollection());
//Act
var result = new DataService(dbConnectionFactoryMock.Object, memCache).GetMessage(1000);
// Assert: has been added to cache
memCache.TryGetValue("Key", out var result2);
Assert.Equal("Some message", result2);
// Assert: value is returned
Assert.Equal("Some message", result);
If you really want to mock it out, here's a guide on how to do that:
Because it's an extension method, you need to make sure that it can be called as is. What happens in your case is that the extension method will call into the mock. Since you provide no expected behavior, it will probably fail.
You need to look at the code for the extension method, check what it accesses and then ensure that your mock complies with the expected behavior. The code is available here:
https://github.com/aspnet/Caching/blob/master/src/Microsoft.Extensions.Caching.Abstractions/MemoryCacheExtensions.cs#L77
This is the code:
public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options)
{
using (var entry = cache.CreateEntry(key))
{
if (options != null)
{
entry.SetOptions(options);
}
entry.Value = value;
}
return value;
}
So, from that, you can see that it accesses CreateEntyand expects an object from it. Then it calls SetOptions and assigns Value on the entry.
You could mock it like this:
var entryMock = new Mock<ICacheEntry>();
memoryCacheMock.Setup(m => m.CreateEntry(It.IsAny<object>())
.Returns(entryMock.Object);
// maybe not needed
entryMock.Setup(e => e.SetOptions(It.IsAny<MemoryCacheEntryOptions>())
...
When you do this, the extension method will be called on the mock and it will return the mocked entry. You can modify the implementation and make it do whatever you want.

Integration tests - what would you test for in this controller?

I'm applying NUnit integration tests on our controller endpoints in a .NET Web API 2 project whose models and controllers are generated via Entity code first from database.
I'm having trouble thinking of what parts of the controller I should test. In the end, we'd just like to be able to automate "can a user with "x" role get this data?"
Looking in the GET portion of this controller, what parts would you test and what's your reasoning?
namespace api.Controllers.myNamespace
{
public class myController : ApiController
{
private string strUserName;
private string strError = "";
private string strApiName = "myTable";
private myDatabase db = new myDatabase();
// ----------------------------------------------------------------------
// GET: api/path
public IQueryable<myTable> GetmyTable()
{
try
{
this.strUserName = this.getUserName();
if
(
// ----- authorize -----
db.view_jnc_role_api_permission.Count
(
view =>
(
view.permission == "get"
&& view.apiName == this.strApiName
&& view.userName == this.strUserName
)
) == 1
// ----- /authorize -----
)
{
// ----- get -----
IQueryable<myTable> data =
from tbl in db.myTable
where tbl.deleted == null
select tbl;
// ----- /get -----
return data;
}
else
{
strError = "Unauthorized.";
throw new HttpResponseException(HttpStatusCode.Forbidden);
}
}
catch (Exception ex)
{
if (strError.Length == 0)
{
if (this.showException())
{
strError = ex.ToString();
}
}
throw new HttpResponseException(ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, strError));
}
}
}
For reference, here's what I have so far. Some of these private fields I'm defining shouldn't be here - currently trying to get access to private methods from my test project via AssemblyInfo.cs to fix this:
namespace api.myNamespace
{
[TestFixture]
public class myController : ApiController
{
private string strUserName;
private string strError = "";
private string strApiName = "myTable";
private myDb db = new myDb();
// Using TransactionScope to (hopefully) prevent integration test's changes to database from persisting
protected TransactionScope TransactionScope;
// Instantiate _controller field
private myController _controller;
[SetUp]
public void SetUp() {
TransactionScope = new TransactionScope(TransactionScopeOption.RequiresNew);
// It's possible that one test may leave some state which could impact subsequent tests - so we must reinstantiate _controller at the start of each new test:
_controller = new myController();
}
[TearDown]
public void TearDown()
{
TransactionScope.Dispose();
}
**//------ TESTS -------//
// CanSetAndGetUserName
// AuthorizedUserCanGetData
// UnauthorizedUserCannotGetData
// AuthorizedUserCanPutData
// UnauthorizedUserCannotPutData
// AuthorizedUserCanPostData
// UnauthorizedUserCannotPostData
// AuthorizedUserCanDeleteData
// UnauthorizedUserCannotDeleteData**
[Test]
public void CanGetAndSetUsername()
{
// ARRANGE
var user = _controller.getUserName();
// ACT
// ASSERT
Assert.That(user, Is.EqualTo("my-internal-username"));
}
[Test]
public void UnauthorizedUserCannotGetData()
{
var user = "Mr Unauthorized";
// Unfinished bc integration testing is super abstract, subjective, hard, time consuming and hard. All downvoters are plebs.
Assert.That(user, Is.EqualTo());
}
}
}
}
integration tests means several things:
you setup your test data in the database, via a script for example.
you call the endpoint under test knowing exactly what data you should call it with and what you should get. This is all based on your test data you setup in step 1.
you compare your expected data with the one you got back.
this is an integration test as it touches everything, both api and database.
Now, you said you are having trouble deciding which parts of the controller to test. This suggests you are confusing integration tests with unit tests.
Integration tests we already covered.
Unit tests cover parts of functionality. You do not test controllers, forget about that.
What you really need to consider doing is this:
First, separate your code from the controller. Keep the controller very basic. It receives a call, validates the request model and passes it further to a class library where the functionality happens. This allows you to forget "testing the controller" and focus on your functionality instead. Unit tests will help here and your test cases will become something like this
I have a user, set up in a certain way.
I have some data, set up in a certain way
When I call method X, then I should get this response.
With such a setup in place, you can set your test data any way you like and check every single test case.
The only reason you wonder how you test your controller is because you dumped all your code into it, which of course makes everything hard. Think SOLID, think SOC ( Separation of concerns ).
One piece of advice: never ever return IQueryable from an endpoint, that's not data, that simply a query that hasn't run yet. Return a List, IEnumerable, an singular object, whatever you need, just make sure you execute that first by calling ToList() for example on your IQueryable expression first.
So, the steps are like this:
Setup your IQueryable first
Execute it by calling ToList(), First(), FirstOrDefault() whatever is appropriate and return the result of that.

MOQ testing Email Service and unable to get back the value

I am trying to test a Domain Service which is to send email after the order has been placed. This service has private methods so I called a method on the public interface which was calling this private service method. Issue is I cant seem to check CC on the email as this is in the private method.
the only approach I know to figure this if that value was saved as an interface property etc but its not. see the code below.
public int SendConsolidatedDespatchNotifications(int countOfWorkDays)
{
var sent = 0;
var trackings = _despatchLineRepository.GetTrackingWithoutDespatchNotificationInPreviousWorkDays(countOfWorkDays);
var trackingsWithinOrder = trackings == null
? new List<OrderLineTracking>()
: trackings.Where(dl => dl.DespatchReference != null).ToList();
trackingsWithinOrder.GroupBy(ot => ot.OrderKey).ForEach(
ot =>
{
if (SendConsolidatedDespatchNotifications(ot))
{
_despatchLineRepository.SetAsSent(ot.Select(ol => ol.DespatchLine));
sent++;
}
});
return sent;
}
private bool SendConsolidatedDespatchNotifications(IGrouping<int, OrderLineTracking> orderTrackingLines)
{
if (orderTrackingLines == null)
return false;
if (orderTrackingLines.Key == 0)
return false;
if (orderTrackingLines.Any())
{
var firstLine = orderTrackingLines.First();
var allOrderLines = _orderLineRepository.GetOrderLinesByOrderKey(firstLine.OrderKey);
var partiallyDespatchedLines = FindPartiallyDespatchedLines(orderTrackingLines);
var notDespatchedLines = FindNotDespatchedLines(allOrderLines, orderTrackingLines);
return SendConsolidatedDespatchedEmail(firstLine.DespatchReference, orderTrackingLines, partiallyDespatchedLines, notDespatchedLines);
}
return false;
}
private bool SendConsolidatedDespatchedEmail(
string poNumber,
IEnumerable<OrderLineTracking> despatchedLines,
IEnumerable<OrderLineTracking> partiallyDespatchedLines,
IEnumerable<OrderLine> notDespatchLines)
{
//we just assume that one PO have always just one order
var firstDespatchedLine = despatchedLines.First();
var order = firstDespatchedLine.OrderLine.OrderHeader;
if (order?.Customer == null)
return false;
var despatchGroups = new List<DespatchLineGroup>();
despatchedLines.GroupBy(dl => dl.DespatchReference).ForEach(
dl => despatchGroups.Add(
new DespatchLineGroup
{
DespatchReference = dl.Key,
DespatchedLines = dl,
TrackingWebLink = GetTrackingWebLinkFor(dl.First())
}));
var despatchNotificationEmail = new DespatchConsolidatedNotificationEmail(
order.Customer,
order,
despatchGroups,
CreateNotDespatchedItemsList(partiallyDespatchedLines, notDespatchLines));
var ccCustomer = _customerRepository.GetByCostCentreIdentifier(order.CostCentreIdentifier, order.Customer.Key);
var ccOnBasket = ccCustomer?.CostCentre;
if (ccOnBasket == null)
{
despatchNotificationEmail.To.Add(new EmailAddress(order.Customer.FullName, order.Customer.Login));
}
else
{
FillInSubaccountDetails(despatchNotificationEmail, ccCustomer, order, order.Customer, ccOnBasket);
}
despatchNotificationEmail.PopulateContentWithTags();
despatchNotificationEmail.SendAfter = firstDespatchedLine.DespatchDate;
despatchNotificationEmail.Save();
_log.InfoFormat("Despatch email {0} for {2} sent to {1}", "DespatchConsolidatedNotificationEmail", order.Customer.Login, poNumber);
return true;
}
private void FillInSubaccountDetails(
EmailTemplate email,
Customer ccCustomer,
OrderHeader order,
Customer masterAccount,
CostCentre ccOnBasket)
{
//send notifications to CostCentre account, which is on basket
email.To.Add(new EmailAddress(ccCustomer.FullName, ccCustomer.Login));
if (ccOnBasket.ReceiveNotifications) //send notifications to master only if CC is set so
{
email.To.Add(new EmailAddress(masterAccount.FullName, masterAccount.Login));
}
if (order.OrderPlacedBy.HasValue) //PD-2140 Sending email to Purchaser as well
{
var purchaser = _customerRepository.Get(order.OrderPlacedBy.Value);
if (purchaser?.Purchaser != null && purchaser.Purchaser.ReceiveNotifications)
{
email.To.Add(new EmailAddress(purchaser.FullName, purchaser.Login));
}
}
if ( order.ApprovedBy != null)
{
var approver = _customerRepository.Get(order.ApprovedBy.Value);
if(approver?.Approver != null && //has approver and its not MAH
approver.Approver.ReceiveNotifications)
email.To.Add(new EmailAddress(approver.FullName, approver.Login));
}
}
//this inherits from EmailTemplate which has save method.
public class DespatchConsolidatedNotificationEmail : EmailTemplate
{
public DespatchConsolidatedNotificationEmail() { }
public DespatchConsolidatedNotificationEmail(
Customer customer,
OrderHeader orderHeader,
List<DespatchLineGroup> despatchLines,
List<NotDespatchedLine> notDespatchLines)
{
AddEmailData(customer);
AddEmailData(orderHeader);
AddEmailData(despatchLines);
AddEmailData(notDespatchLines);
}
}
//below is the save method
public int Save()
{
var manageSave = Configuration.Container.Resolve<IWantToManageSaving>();
return manageSave.Save(this);
}
Note Email implements a abstract class which is EmailTemplate not an interface..
I want to figure out which emailAddress has been added ?
There are arguments pro and con unit testing private methods. I'll leave it up to you to decide if it's a good idea or not. Having said that you can use the PrivateObject class. Something along these lines:
Class c = new Class();
PrivateObject o = new PrivateObject(c);
var whatever = o.Invoke("FillInSubaccountDetails");
Assert.AreEqual(whatever, expected);
There is a problem here since your method returns void, there's no return value to assert. You may need to adapt your method?
So, based on all the code you provided:
Your Save method smells of bad practice. You shouldn't use an IoC container to manually resolve dependencies. If you had the IWantToManageSaving (I like the naming by the way :) ) injected via a ctor you could mock it in your test. If you had a var savingManagerMock = new Mock<IWantToManageSaving>(), you could then verify in your unit test that the Save method was called with a correctly setup instance of EmailTemplate. Something like:
// ASSERT
savingManagerMock.Verify(x => x.Save(It.IsAny<EmailTemplate>(
arg => arg.To.Contains(/* ... */));
Or something or the like, depends on the actual assertions you want.
Another way would be to abstract the construction of the DespatchConsolidatedNotificationEmail into a factory IDespatchConsolidatedNotificationEmailFactory, make it return a special mock of DespatchConsolidatedNotificationEmail and setup its Save method to, for example, save the current state of the EmailTemplate and then assert it. I would still lean towards the first solution, however.
On an ending note: as you can see testing this method is fairly complicated. That usually means it could be written better. In this case I see two red flags, first one is the explicit use of a Container which can always be avoided with dependency injection (manual resolution ain't no injection :P). Second is that this method is fairly complicated! It calls a lot of private methods and there's a lot of logic that can't be understood by a quick read through the method. You should consider splitting these private methods into maybe internal helper methods in another class that would be unit-tested separately. Then you could trust them when testing this public method, as they're basicaly a dependency at that point, setup mocks and just assert that correct internal methods are called and the method's contract is fulfilled.

FakeItEasy Action parameter in UnitTest, but still execute inner Action code

I'm currently making some UnitTests for some new features I've added to our ASP.NET project (no it's not test-driving design). We use the NHibernate framework and use the UnitTest Mock-ing library FakeItEasy.
I have the following class & method which I want to test:
public class Round
{
public static Round Create(List<Company> activeCompanies, Period period,
BusinessUser user, BusinessUser systemUser,
ISession session, IEntityQuery entityQuery,
RoundProcessBuilder processBuilder)
{
var round = new Round
{
Processes = new List<Process>();
Period = period,
CreationDate = DateTime.Now,
CreatedBy = user
};
// Save the Round in the DB so we can use it's Id in the Processes:
session.Save(round);
foreach (var company in activeCompanies)
{
var companyData = session.Get<CompanyData>(company.Id);
var processResult =
roundProcessBuilder.Build(
systemUser,
new CreateRoundProcessData(company, round, companyData),
entityQuery,
session);
processResult.HandleProcess(process =>
{
// serviceBus can stay null
process.Create(systemUser, DateTime.Now, session, null);
// No need to save the session here. If something went
// wrong we don't want halve of the processes being saved
round.Processes.Add(process);
// It's all or nothing
});
}
return round;
}
}
What I mainly want to test: When I use this Round#Create method with let's say 100 active companies, it should create 100 processes, and each of those processes should contain the RoundId.
This is my UnitTest so far:
[TestFixture]
public class RoundTest
{
private BusinessUser _systemUser;
private DateTime _creationDateRound1;
private List<Company> _activeCompanies;
private RoundProcessBuilder _roundProcessBuilder;
private ISession _session;
[SetUp]
public void Setup()
{
_creationDateRound1 = new DateTime(2015, 10, 5);
_systemUser = TestHelper.CreateBusinessUser(Role.Create("systemuser", "test",
Int32.MaxValue));
_activeCompanies = new List<Company>
{
TestHelper.CreateCompany();
};
_roundProcessBuilder = A.Fake<RoundProcessBuilder>();
_session = A.Fake<ISession>();
}
[Test]
public void TestCreateRoundWithoutPreviousRound()
{
var fakeExpectedRound = Round.Create(_activeCompanies, DateTime.Now.ToPeriod(),
_systemUser, _systemUser, _session, null, _roundProcessBuilder);
var fakeExpectedRoundData = RoundProcessData.Create(TestHelper.CreateCompany(),
fakeExpectedRound, new CompanyData());
var fakeExpectedProcess = new Process(_systemUser, null, "processName", null,
fakeExpectedRoundData, "controllerName", null);
var processSuccessResult = new ProcessSuccessResult(fakeExpectedProcess);
A.CallTo(() => _roundProcessBuilder.Build(null, null, null, null))
.WithAnyArguments()
.Returns(processSuccessResult);
A.CallTo(() => processSuccessResult.HandleProcess(A<Action<Process>>.Ignored))
.Invokes((Action<Process> action) => action(fakeExpectedProcess));
var round = Round.Create(_activeCompanies, _ceationDateRound1.ToPeriod(),
_systemUser, _systemUser, _session, null, _roundProcessBuilder);
Assert.AreEqual(_activeCompanies.Count, round.Processes.Count, "Number of processes");
Assert.AreEqual(round.Period.Quarter, Math.Ceiling(_creationDateRound1.Month / 3.0m), "Quarter");
Assert.AreEqual(round.Period.Year, round.Year, "Year");
// Test if each of the processes knows the RoundId, have the proper state,
// and are assigned to the systemuser
//foreach (var process in round.Processes)
//{
// var roundProcessData = process.ProcessData as RoundProcessData;
// Assert.IsNotNull(roundProcessData, "All processes should have RoundProcessData-objects as their data-object");
// Assert.AreEqual(roundProcessData.Round.Id, round.Id, "RoundId");
// Assert.AreEqual(process.Phase.State, PhaseState.Start, "Process state should be Start");
// Assert.AreEqual(process.AssignedTo, _systemUser, "AssignedTo should be systemuser");
//}
}
... // More tests
}
My problem lies in the following code:
A.CallTo(() => processSuccessResult.HandleProcess(A<Action<Process>>.Ignored))
.Invokes((Action<Process> action) => action(fakeExpectedProcess));
It gives an "The specified object is not recognized as a fake object." error.
The reason I have this part of the code is because the process in the following part was null without it:
processResult.HandleProcess(process => // <- this was null
{
process.Create(systemUser, DateTime.Now, session, null);
round.Processes.Add(process);
});
PS: I uncommented the foreach with additional checks in my UnitTest because it most likely is pretty useless anyway when I mock the process itself.. My main test is if processes are created and added to the list based on the active companies given.
Your problem seems to be that you are trying to add "fake" logic to an object that is not in fact, a fake:
// You create this as an instance of ProcessSuccessResult:
var processSuccessResult = new ProcessSuccessResult(fakeExpectedProcess);
...then proceed to attempt to add a condition to it here:
A.CallTo(() =>
processSuccessResult
.HandleProcess(A<Action<Process>>.Ignored))
.Invokes((Action<Process> action) => action(fakeExpectedProcess));
In order to do this last bit, the variable processSuccessResult will need to be a fake instance of an interface, so that FakeItEasy can work with it, and apply the logic you want.
I'm assuming ProcessSuccessResult is a class you have access to, and are able to edit? If so, you should be able to add an interface to it, that will contain the methods you need, so you can work against that later.
Once you've defined that, you should be able to create your fake object as follows, where IProcessSuccessResult will be a fake implementation of your interface, provided by FakeItEasy:
var processSuccessResult = A.Fake<IProcessSuccessResult>();
Now you should be able to add logic to that fake object using A.CallTo(...).
Of course, this will imply that the real implementation of your class ProcessSuccessResult is not included or called via the variable processSuccessResult. If part of it needs to be, then you might try to either:
Add logic similar to it, or calls to it from the fake object using FakeItEasy's set up code (although this might get overly complicated), OR:
Add a separate variable to contain an instance of the real class (i.e. two variables fakeProcessSuccessResult and processSuccessResult, respectively), and use separate tests for testing separate aspects of your both this class, and it's usages.
I would recommend the latter, if possible.
I hope this is clear enough, and that this will be useful to you. I know it can be quite complicated sometimes, to find the optimal strategy for testing things like this.

Categories

Resources