Roni
Roni

Reputation: 403

FakeItEasy - How to verify nested arguments value C#

I need your help in order to find a way of verifying the value of nested objects passed as a parameter of the method under test invocation. Assume this class:

public class AuditTrailValueObject
{
    public ActionType Action { get; private set; }
    public EntityType EntityType { get; private set; }
    public long EntityId { get; private set; }
    public DateTime StartTime { get; private set; }
    public bool IsSuccess { get; private set; }
    public string Remarks { get; private set; }

    public AuditTrailValueObject(ActionType action, EntityType entityType, long entityId, DateTime startTime, bool isSuccess, string remarks = "")
    {
        Action = action;
        EntityType = entityType;
        EntityId = entityId;
        StartTime = startTime;
        IsSuccess = isSuccess;
        Remarks = remarks;
    }
}

And the following interface has this class as an injected dependency:

public interface IAuditTrailService
{
    void WriteToAuditTrail(AuditTrailValueObject auditParamData);
}

Now I have the ScanService depending on the AuditTrailService (which implements IAuditTrailService):

public long CreateScanRequest(long projectId)
{
    ScanRequestWriteModel scanRequest = _scanRequestWriteModelFactory.Create(projectDetails);

    long scanRequestId = _scanRequestsWriteRepository.Insert(scanRequest);

    _auditTrailService.WriteToAuditTrail(new AuditTrailValueObject(ActionType.Run, EntityType.SastScanRequest, scanRequestId, DateTime.UtcNow, true));

    return scanRequestId;
}

The test I've written:

[TestMethod]
public void Scan_GivenProjectId_ShouldAuditSuccess()
{
    //Given
    var projectId = 100;

    var scanService = CreateScanService();

    ...
    A.CallTo(() => _scanRequestWriteModelFactory.Create(projectDetails)).Returns(new ScanRequestWriteModel());
    A.CallTo(() => _scanRequestsWriteRepository.Insert(A<ScanRequestWriteModel>._)).Returns(1);

    //When
    var scanRequestId = scanService.CreateScanRequest(projectId);

    //Then
     A.CallTo(() => _auditTrailService.WriteToAuditTrail(
                        new AuditTrailValueObject(ActionType.Run, EntityType.SastScanRequest, scanRequestId, A<DateTime>._, true, A<string>._))).MustHaveHappened();
}

When running this test I'm getting:

System.InvalidCastException: Specified cast is not valid

How can I verify the value of a nested parameter in AuditTrailValueObject?

Upvotes: 2

Views: 1630

Answers (2)

tom redfern
tom redfern

Reputation: 31750

Your problem is a symptom of a larger problem: you are trying to do too much with one test.

Because you're newing-up an instance of AuditTrailValueObject in your WriteToAuditTrail() method, you will have no means of accessing this object instance as it is created within the method scope and is therefore immune to inspection.

However, it appears that the only reason you wish to access this object in the first place is so that you can verify that the values being set within it are correct.

Of these values, only one (as far as your code sample allows us to know) is set from within the calling method. This is the return value from the call made to _scanRequestsWriteRepository.Insert(), which should be the subject of its own unit test where you can verify correct behaviour independently of where it is being used.

Writing this unit test (on the _scanRequestsWriteRepository.Insert() method) will actually address the underlying cause of your problem (that you are doing too much with a single test). Your immediate problem, however, still needs to be addressed. The simplest way of doing this is to remove the AuditTrailValueObject class entirely, and just pass your arguments directly to the call to WriteToAuditTrail().

If I'll remove AuditTrailValueObject where the place should I verify what params are being passed to the auditTrailService? What I mean is that also if I've tested the auditTrailService I need to know that scan service call if with the right parameters (for example: with ActionType.Run and not with ActionType.Update).

To verify that the correct parameters have been passed to the call to WriteToAuditTrail() you can inject a fake of IAuditTrailService and verify your call has happened:

A.CallTo(
    () => _auditTrailService.WriteToAuditTrail(
                    ActionType.Run, 
                    EntityType.SastScanRequest, 
                    scanRequestId, 
                    myDateTime, 
                    true, 
                    myString)
).MustHaveHappened();

Upvotes: 2

Blair Conrad
Blair Conrad

Reputation: 241714

@tom redfern makes many good points, which you may want to address. But after rereading your code and comments, I think I an immediate way forward. Your code has at least one problem, and it may have another.

Let's look at

A.CallTo(() => _auditTrailService.WriteToAuditTrail(
                        new AuditTrailValueObject(ActionType.Run,
                                                  EntityType.SastScanRequest,
                                                  scanRequestId,
                                                  A<DateTime>._,
                                                  true
                                                  A<string>._)))
        .MustHaveHappened();

The _ constructs are being used here inside the AuditTrailValueObject constructor, and they are not valid there. They'll result in default values being assigned to the AuditTrailValueObject, (DateTime.MinValue and null, I think), and are almost not what you want. if you extract the new out to the previous line, you'll see FakeItEasy throw an error when _ is used. I think that it should do a better job of helping you find the problem in your code, but I'm not sure it's possible. I've created FakeItEasy Issue 1177 - Argument constraint That, when nested deeper in A.CallTo, misreports what's being matched to help FakeItEasy improve.

Related to this is how FakeItEasy matches objects. When provided with a value to compare, (the result of new AuditTrailValueObject(…)) FakeItEasy will use Equals to compare the object against the received parameter. Unless your AuditTrailValueObject has a good Equals, this will fail.

If you want to keep using AuditTrailValueObject and don't want to provide an Equals (that would ignore the startTime and the remarks), there are ways forward.

First, you could use That.Matches, like so:

A.CallTo(() => _auditTrailService.WriteToAuditTrail(A<AuditTrailValueObject>.That.Matches(
                                                        a => a.Action == ActionType.Run &&
                                                        a.EntityType == EntityType.SastScanRequest &&
                                                        a.EntityId == scanRequestId &&
                                                        a.IsSuccess)))
                                                    .MustHaveHappened();

Some people aren't wild about complex constraints in the Matches, so an alternative is to capture the AuditTrailValueObject and interrogate it later, as Alex James Brown has described in his answer to Why can't I capture a FakeItEasy expectation in a variable?.

Upvotes: 4

Related Questions