J'utilise asp net core 1.0 et xunit.
J'essaie d'écrire un test unitaire pour un code qui utilise IMemoryCache
. Cependant, chaque fois que j'essaie de définir une valeur dans IMemoryCache
, j'obtiens une erreur de référence nulle.
Mon code de test unitaire est comme ceci:
Le IMemoryCache
est injecté dans la classe que je veux tester. Cependant, lorsque j'essaie de définir une valeur dans le cache lors du test, j'obtiens une référence nulle.
public Test GetSystemUnderTest()
{
var mockCache = new Mock<IMemoryCache>();
return new Test(mockCache.Object);
}
[Fact]
public void TestCache()
{
var sut = GetSystemUnderTest();
sut.SetCache("key", "value"); //NULL Reference thrown here
}
Et ceci est le test de classe ...
public class Test
{
private readonly IMemoryCache _memoryCache;
public Test(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public void SetCache(string key, string value)
{
_memoryCache.Set(key, value, new MemoryCacheEntryOptions {SlidingExpiration = TimeSpan.FromHours(1)});
}
}
Ma question est ... Dois-je configurer le IMemoryCache
d'une manière ou d'une autre? Définissez une valeur pour DefaultValue? Lorsque IMemoryCache
est Mocked quelle est la valeur par défaut?
IMemoryCache.Set
Est une méthode d'extension et ne peut donc pas être moquée en utilisant le framework Moq .
Le code de l'extension est cependant disponible ici
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;
}
Pour le test, un chemin sûr devrait être simulé par la méthode d'extension pour lui permettre de s'écouler jusqu'à son terme. Dans Set
, il appelle également des méthodes d'extension sur l'entrée du cache, ce qui devra également être pris en charge. Cela peut se compliquer très rapidement, donc je suggère d'utiliser une implémentation concrète
//...
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
//...
public Test GetSystemUnderTest() {
var services = new ServiceCollection();
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var memoryCache = serviceProvider.GetService<IMemoryCache>();
return new Test(memoryCache);
}
[Fact]
public void TestCache() {
//Arrange
var sut = GetSystemUnderTest();
//Act
sut.SetCache("key", "value");
//Assert
//...
}
Alors maintenant, vous avez accès à un cache mémoire entièrement fonctionnel.
Faites défiler vers le bas pour l'extrait de code pour se moquer indirectement du setter de cache (avec une propriété d'expiration différente)
Bien qu'il soit vrai que les méthodes d'extension ne peuvent pas être moquées directement en utilisant Moq ou la plupart des autres cadres de moquerie, elles peuvent souvent être moquées indirectement - et c'est certainement le cas pour ceux construits autour de IMemoryCache
Comme je l'ai souligné dans cette réponse , fondamentalement, toutes les méthodes d'extension appellent l'une des trois méthodes d'interface quelque part dans leur exécution.
Nkosi'sanswer soulève des points très valables: cela peut se compliquer très rapidement et vous pouvez utiliser une implémentation concrète pour tester les choses. Il s'agit d'une approche parfaitement valable à utiliser. Cependant, à strictement parler, si vous suivez cette voie, vos tests dépendront de l'implémentation de code tiers. En théorie, il est possible que les modifications apportées à cela cassent vos tests - dans cette situation, il est très peu probable que cela se produise car le référentiel caching a été archivé.
De plus, il est possible que l'utilisation d'une implémentation concrète avec un tas de dépendances puisse impliquer beaucoup de frais généraux. Si vous créez un ensemble de dépendances propre à chaque fois et que vous avez de nombreux tests, cela pourrait ajouter une charge importante à votre serveur de build (je ne dis pas que c'est le cas ici, cela dépendrait d'un certain nombre de facteurs)
Enfin, vous perdez un autre avantage: en enquêtant vous-même sur le code source afin de vous moquer des bonnes choses, vous êtes plus susceptible de découvrir le fonctionnement de la bibliothèque que vous utilisez. Par conséquent, vous pourriez apprendre à mieux l'utiliser et vous apprendrez certainement d'autres choses.
Pour la méthode d'extension que vous appelez, vous ne devriez avoir besoin que de trois appels de configuration avec rappels pour faire valoir les arguments d'invocation. Cela peut ne pas vous convenir, selon ce que vous essayez de tester.
[Fact]
public void TestMethod()
{
var expectedKey = "expectedKey";
var expectedValue = "expectedValue";
var expectedMilliseconds = 100;
var mockCache = new Mock<IMemoryCache>();
var mockCacheEntry = new Mock<ICacheEntry>();
string? keyPayload = null;
mockCache
.Setup(mc => mc.CreateEntry(It.IsAny<object>()))
.Callback((object k) => keyPayload = (string)k)
.Returns(mockCacheEntry.Object); // this should address your null reference exception
object? valuePayload = null;
mockCacheEntry
.SetupSet(mce => mce.Value = It.IsAny<object>())
.Callback<object>(v => valuePayload = v);
TimeSpan? expirationPayload = null;
mockCacheEntry
.SetupSet(mce => mce.AbsoluteExpirationRelativeToNow = It.IsAny<TimeSpan?>())
.Callback<TimeSpan?>(dto => expirationPayload = dto);
// Act
var success = _target.SetCacheValue(expectedKey, expectedValue,
new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMilliseconds(expectedMilliseconds)));
// Assert
Assert.True(success);
Assert.Equal("key", keyPayload);
Assert.Equal("expectedValue", valuePayload as string);
Assert.Equal(expirationPayload, TimeSpan.FromMilliseconds(expectedMilliseconds));
}
J'ai eu un problème similaire, mais je souhaite désactiver la mise en cache pour le débogage de temps en temps, car il est difficile de continuer à vider le cache. Il suffit de les simuler/simuler vous-même (en utilisant StructureMap
injection de dépendance).
Vous pouvez également les utiliser facilement dans vos tests.
public class DefaultRegistry: Registry
{
public static IConfiguration Configuration = new ConfigurationBuilder()
.SetBasePath(HttpRuntime.AppDomainAppPath)
.AddJsonFile("appsettings.json")
.Build();
public DefaultRegistry()
{
For<IConfiguration>().Use(() => Configuration);
#if DEBUG && DISABLE_CACHE <-- compiler directives
For<IMemoryCache>().Use(
() => new MemoryCacheFake()
).Singleton();
#else
var memoryCacheOptions = new MemoryCacheOptions();
For<IMemoryCache>().Use(
() => new MemoryCache(Options.Create(memoryCacheOptions))
).Singleton();
#endif
For<SKiNDbContext>().Use(() => new SKiNDbContextFactory().CreateDbContext(Configuration));
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
});
}
}
public class MemoryCacheFake : IMemoryCache
{
public ICacheEntry CreateEntry(object key)
{
return new CacheEntryFake { Key = key };
}
public void Dispose()
{
}
public void Remove(object key)
{
}
public bool TryGetValue(object key, out object value)
{
value = null;
return false;
}
}
public class CacheEntryFake : ICacheEntry
{
public object Key {get; set;}
public object Value { get; set; }
public DateTimeOffset? AbsoluteExpiration { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
public TimeSpan? SlidingExpiration { get; set; }
public IList<IChangeToken> ExpirationTokens { get; set; }
public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; set; }
public CacheItemPriority Priority { get; set; }
public long? Size { get; set; }
public void Dispose()
{
}
}