Skip to content

Instantly share code, notes, and snippets.

@Zodt
Last active July 6, 2023 22:50
Show Gist options
  • Save Zodt/a533190640ed73e0435e1e4ae2ec352a to your computer and use it in GitHub Desktop.
Save Zodt/a533190640ed73e0435e1e4ae2ec352a to your computer and use it in GitHub Desktop.
A basic representation of the implementation of the interceptor functionality in unit tests.

Based on the materials of YouTube video by Nick Chapsas (@Elfocrash)

How should it work?

InterceptionFromAttribute

This attribute indicates to the source code generator the type and its method in which certain methods specified in the Intercepted<TInterception, TReturn> arguments should be intercepted The source code generator can get the following data from this data:

  • Line numbers and the path to the file containing this type;
  • Return value of the method;
  • Synchronous or asynchronous method.

InterceptedAttribute<TInterception, TReturn>

This attribute specifies the following data to the source code generator:

  • Intercepted type - TInterception;
  • Return value type - TReturn;
  • The name of the intercepted method - interceptionMethodName;
  • The required return value - returnValue. This value must be nullable.

Also, the constructor of this attribute must have 2 overloads: with and without the return value. In the second case, the source code generator should automatically return void or non-generic classes Task or ValueTask when the call in the method under test is not essential for the test itself.

Unexplored questions

  • How will the compiler understand which generated method to use? For example, if you run both tests from option 1, how should the compiler understand that some methods are needed in the first test, and others in the second, if both intercept the same lines of code?
  • Would it be right to move away from the usual practice of writing unit tests, where one service class (for example) tests one test class and all its methods? Or would a separation similar to the REPR pattern be appropriate in this case when one test class is responsible for only one test method of the service?
public sealed class MovieServiceTest
{
[InterceptionFrom<MovieService>(methodName: nameof(MovieService.Create))]
[Intercepted<MovieRepository, bool>(interceptionMethodName: nameof(MovieRepository.CreateAsync), returnValue: "false")]
[Intercepted<MovieRepository, Movie>(interceptionMethodName: nameof(MovieRepository.GetBySlugAsync), returnValue: "new()")]
public async Task Create_ShouldReturnValidationError_WhenMovieExists()
{
// some code
}
[InterceptionFrom<MovieService>(methodName: nameof(MovieService.Create))]
[Intercepted<MovieRepository, bool>(interceptionMethodName: nameof(MovieRepository.CreateAsync), returnValue: "true")]
[Intercepted<MovieRepository, Movie>(interceptionMethodName: nameof(MovieRepository.GetBySlugAsync), returnValue: "null")]
public async Task Create_ShouldReturnTrue_WhenMovieNotExists()
{
// some code
}
}
// generated code
internal sealed partial class MovieRepositoryExtensions
{
#region Create_ShouldReturnTrue_WhenMovieNotExists
[InterceptesLocation("""some path""", 28, 52]
public async Task<Movie?> Create_ShouldReturnTrue_WhenMovieNotExists_MockGetBySlugAsync() =>
return Task.FromResult<Movie?>(null);
[InterceptesLocation("""some path""", 28, 64]
public async Task<bool> Create_ShouldReturnTrue_WhenMovieNotExists_MockCreateAsync() =>
return Task.FromResult(true);
#endregion
#region Create_ShouldReturnValidationError_WhenMovieExists
[InterceptesLocation("""some path""", 28, 52]
public async Task<Movie?> Create_ShouldReturnValidationError_WhenMovieExists_MockGetBySlugAsync() =>
return Task.FromResult<Movie?>(new Movie());
[InterceptesLocation("""some path""", 28, 64]
public async Task<bool> Create_ShouldReturnValidationError_WhenMovieExists_MockCreateAsync() =>
return Task.FromResult(false);
#endregion
}
[InterceptionFrom<MovieService>(methodName: nameof(MovieService.Create))]
public sealed class MovieServiceCreateTest
{
[Intercepted<MovieRepository, bool>(interceptionMethodName: nameof(MovieRepository.CreateAsync), returnValue: "false")]
[Intercepted<MovieRepository, Movie>(interceptionMethodName: nameof(MovieRepository.GetBySlugAsync), returnValue: "new()")]
public async Task Create_ShouldReturnValidationError_WhenMovieExists()
{
// some code
}
[Intercepted<MovieRepository, bool>(interceptionMethodName: nameof(MovieRepository.CreateAsync), returnValue: "true")]
[Intercepted<MovieRepository, Movie>(interceptionMethodName: nameof(MovieRepository.GetBySlugAsync), returnValue: "null")]
public async Task Create_ShouldReturnTrue_WhenMovieNotExists()
{
// some code
}
}
// generated code
internal sealed partial class MovieRepositoryExtensions
{
#region Create_ShouldReturnTrue_WhenMovieNotExists
[InterceptesLocation("""some path""", 28, 52]
public async Task<Movie?> Create_ShouldReturnTrue_WhenMovieNotExists_MockGetBySlugAsync() =>
return Task.FromResult<Movie?>(null);
[InterceptesLocation("""some path""", 28, 64]
public async Task<bool> Create_ShouldReturnTrue_WhenMovieNotExists_MockCreateAsync() =>
return Task.FromResult(true);
#endregion
#region Create_ShouldReturnValidationError_WhenMovieExists
[InterceptesLocation("""some path""", 28, 52]
public async Task<Movie?> Create_ShouldReturnValidationError_WhenMovieExists_MockGetBySlugAsync() =>
return Task.FromResult<Movie?>(new Movie());
[InterceptesLocation("""some path""", 28, 64]
public async Task<bool> Create_ShouldReturnValidationError_WhenMovieExists_MockCreateAsync() =>
return Task.FromResult(false);
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment