postsharp / Metalama

Metalama is a Roslyn-based meta-programming framework. Use this repo to report bugs or ask questions.
164 stars 4 forks source link

[Question] - How to do unit tests when an aspect uses [IntroduceDependency] #319

Closed hazzinator1 closed 1 week ago

hazzinator1 commented 1 week ago

Hi, I'm a little confused as to how I execute unit tests when one of the classes I'm unit testing has an aspect that uses the [IntroduceDependency] attribute.

I am currently getting this error Initialization method Initialize threw exception. System.ArgumentNullException: Value cannot be null. (Parameter 'unitOfWork').

This is because [SaveChanges] brings through UnitOfWork as an [IntroduceDependency] (see below code examples).

public partial class SaveMyAttachmentAsNewVersionService
{
    private IOnlineDocumentRepository OnlineDocumentRepository { get; }
    private IGraphAttachmentRepository GraphAttachmentRepository { get; }
    private IUserExternalService UserService { get; }
    private RecentProvisionableHistoryLoader RecentProvisionableHistoryLoader { get; }
    private ProvisionableLoader ProvisionableLoader { get; }

    public SaveMyAttachmentAsNewVersionService(
        IOnlineDocumentRepository onlineDocumentRepository,
        IGraphAttachmentRepository graphAttachmentRepository,
        IUserExternalService userService,
        RecentProvisionableHistoryLoader recentProvisionableHistoryLoader,
        ProvisionableLoader provisionableLoader)
    {
        this.OnlineDocumentRepository = onlineDocumentRepository;
        this.GraphAttachmentRepository = graphAttachmentRepository;
        this.UserService = userService;
        this.RecentProvisionableHistoryLoader = recentProvisionableHistoryLoader;
        this.ProvisionableLoader = provisionableLoader;
    }

    [SaveChanges]
    public async Task SaveAttachmentAsNewVersion(SaveMyAttachmentAsNewVersionRequest request)
    {
        ...
    }
}
public class SaveChangesAttribute : OverrideMethodAspect
{
    [IntroduceDependency(IsRequired = true)]
    private readonly IUnitOfWork UnitOfWork;

    public override dynamic? OverrideMethod()
    {
        throw new Exception("SaveChanges cannot be applied to non-async methods");
    }

    public override async Task<dynamic?> OverrideAsyncMethod()
    {
        var result = await meta.Proceed();
        await this.UnitOfWork.SaveChanges();
        return result;
    }
}

I notice docs like https://doc.postsharp.net/metalama/conceptual/aspects/testing/compile-time-testing seem to mention using XUnit, while we are using MSTest. It would be good if there was a way to avoid needing to swap over to XUnit just to fix these issues, as otherwise everything else should be working.

prochan2 commented 1 week ago

Hello, unit testing types enhanced using the [IntroduceDependency] attribute works the same way as unit testing any other types that expect dependency injection to inject dependencies via the constructor. In your code, the SaveMyAttachmentAsNewVersionService constructor expects dependencies like IOnlineDocumentRepository to be provided. By applying the [SaveChanges] aspect on a method within the SaveMyAttachmentAsNewVersionService type, Metalama adds the IUnitOfWork dependency to the constructor parameters, so you need to provide this dependency the same way as you're providing the other dependencies like the IOnlineDocumentRepository.

For run-time unit tests, you don't need to use XUnit. This is only necessary for compile-time tests, that don't execute your code, but compare code rewritten by Metalama with an expected code, as described at https://doc.postsharp.net/metalama/conceptual/aspects/testing/compile-time-testing .

gfraiteur commented 1 week ago

Note that you should make your class partial if you want to use the new constructor explicitly, otherwise you need to instantiate your class through ServiceCollection.

hazzinator1 commented 1 week ago

Great, thanks for the info guys! Will close this off now