2

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?

Roni
  • 369
  • 1
  • 7
  • 22
  • Are you able to tell us where the error occurs? Do you have a stack trace? Anything that would narrow this down? You've dumped an awful lot of code in this question. Perhaps consult https://stackoverflow.com/help/how-to-ask and rephrase the question a bit. I _did_ take all your code and try to reproduce. I had some problems because there are a lot of bits left out, and I had to make guesses. Also your test doesn't compile: I think you have a duplicate "A.CallTo(() => _auditTrailService.WriteToAuditTrail(" line. If you correct these problems and provide full test output, I'll try again. – Blair Conrad Jul 31 '17 at 11:02
  • @BlairConrad I've fixed the wrong line you noticed. Would you like I'll upload the all missed lines (it could be more tiring...)? – Roni Jul 31 '17 at 11:19
  • I think best would be to indicate the source of the invalid cast. If you're catching an exception, please include the exception, including the whole stack trace. (And given the amount of code you're already including, more isn't so much worse. I'd say either whittle away extra bits of code until you have a very focused example, or include enough to compile and run.) – Blair Conrad Jul 31 '17 at 11:23
  • @BlairConrad I know the problem cames from this `A.CallTo(() => _auditTrailService.WriteToAuditTrail( **new** AuditTrailValueObject` (As @tomredffern explained). And I'm looking for another way to verify the values `scanService` pass to the `writeToAuditTrail` method. – Roni Jul 31 '17 at 11:38

2 Answers2

3

@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?.

Blair Conrad
  • 233,004
  • 25
  • 132
  • 111
2

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();
tom redfern
  • 30,562
  • 14
  • 91
  • 126
  • @tomredffern Conceptually, I agree with you. I'm not sure I understand exactly what you're mean. The values I want to verify are `AuditTrailValueObject` fields such as `ActionType.Run`, `EntityType.SastScanRequest`. those values are not part of the return object from the `_scanRequestsWriteRepository.Insert()` invocation. So my question, if I'll not use the changes you suggested how can I verify those values? – Roni Jul 31 '17 at 11:33
  • @Roni - I know which values you are trying to verify. However, I am saying that this is not the place to do such verification. The place to verify these values is at the point where they are created, not when they are being used. Additionally, I'm saying that you could go further and remove the AuditTrailValueObject object altogether. I don't see a reason for its existence in the code. You don't need it - it doesn't do anything other than store values. – tom redfern Jul 31 '17 at 12:39
  • 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`). Thanks! – Roni Aug 02 '17 at 06:41
  • 1
    @Roni - 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. See update to my answer – tom redfern Aug 02 '17 at 07:49