I have a singleton class and I want to run some unit tests in isolation. The problem is that all the tests run on the same instance and affect the expected results. Is there a workaround? I am using .net 6 and NUnit 3. Here's my code,
public interface ISingletonModel<T>
{
void AddValue(string key, T value, LinkedListNode node);
}
public sealed class SingletonModel<T> : ISingletonModel<T>
{
public Dictionary<string, (LinkedListNode<string> node, T value)> ModelDictionary { get; set; }
private static readonly Lazy<SingletonModel<T>> singletonModel=
new Lazy<SingletonModel<T>>(() => new SingletonModel<T>());
public static SingletonModel<T> Instance
{
get
{
return SingletonModel.Value;
}
}
private SingletonModel()
{
ModelDictionary = new Dictionary<string, (node, T value)>();
}
}
public void AddValue(string key, T value, LinkedListNode node)
{
if (ModelDictionary.ContainsKey(key))
{
var linkedListNode = ModelDictionary[key];
ModelDictionary[key] = (node, value);
}
else
{
ModelDictionary.Add(key, (node.AddFirst(key), value));
}
}
}
And some unit tests
private TestClass[] testData;
private IFixture fixture;
private SingletonModel<TestClass> model;
[SetUp]
public void Setup()
{
this.testData = GenerateTestData();
this.model= SingletonModel<TestClass>.Instance;
}
[Test]
public void CheckModelCapacity_ShouldReturnTheCorrectItems()
{
/ Act
foreach (var entity in testData)
{
this.model.AddValue(entity.Id, entity, entity.node);
}
IEnumerable<string> expected = new[] { "8", "3", "5", "2", "79" };
var actual = this.model.ModelDictionary.Keys.ToList();
// Assert
Assert.That(expected.OrderBy(x => x).SequenceEqual(actual.OrderBy(x => x)));
}
[Test]
public void CheckTotalItems_ShouldReplaceItemsWithTheSameKey()
{
// Assign
var entity1 = fixture.CreateMany(20);
// Act
foreach (var item in entity1)
{
this.model.AddValue(item.Id, item, item.node);
}
//Assert
Assert.AreEqual(2, this.model.ModelDictionary.Count);
}
Because of the singleton the tests are holding the values from the previous tests.
CodePudding user response:
A public
or internal
constructor would allow for creating isolated instances in tests, but if the intention here is to not modify the current class then reflection can be used to access the private constructor to create isolated instances for tests.
Here is a simplified model based on the original since it was incomplete and wouldn't compile.
public interface ISingletonModel<T> {
void AddValue(string key, T value);
int Count { get; }
}
public sealed class SingletonModel<T> : ISingletonModel<T> {
Dictionary<string, T> dictionary;
private static readonly Lazy<SingletonModel<T>> singletonModel = new Lazy<SingletonModel<T>>(() => new SingletonModel<T>());
public static SingletonModel<T> Instance => singletonModel.Value;
private SingletonModel() {
dictionary = new Dictionary<string, T>();
}
public void AddValue(string key, T value) => dictionary[key] = value;
public int Count => dictionary.Count;
}
Using one of the overloads of Activator.CreateInstance:
Activator.CreateInstance(Type type, bool nonPublic)
which uses reflection to access the private constructor, shows that instances can be created to be tested in isolation
public class Program {
public static void Main() {
//Arrange
int expected = 1;
ISingletonModel<int> model1 = (ISingletonModel<int>)Activator.CreateInstance(typeof(SingletonModel<int>), true);
//Act
model1.AddValue("one", 1);
//Assert
int actual = model1.Count;
Console.WriteLine($"{actual}, As expected: {actual == expected}");
//Arrange
expected = 3;
ISingletonModel<int> model2 = (ISingletonModel<int>)Activator.CreateInstance(typeof(SingletonModel<int>), true);
//Act
model2.AddValue("one", 1);
model2.AddValue("two", 2);
model2.AddValue("three", 3);
//Assert
actual = model2.Count;
Console.WriteLine($"{actual}, As expected: {actual == expected}");
//Arrange
expected = 2;
ISingletonModel<int> model3 = (ISingletonModel<int>)Activator.CreateInstance(typeof(SingletonModel<int>), true);
//Act
model3.AddValue("one", 1);
model3.AddValue("two", 2);
//Assert
actual = model3.Count;
Console.WriteLine($"{actual}, As expected: {actual == expected}");
}
}
which return the following output
1, As expected: True
3, As expected: True
2, As expected: True
CodePudding user response:
Add ResetForTesting() method your singleton class?