前言
文章同步在公众号与知乎更新,账号全部同名。搜索"小黄花呀小黄花"即可找到我。
前面已经基本完成了Common部分的代码编写。前面的文章全部可以前往专栏查看。后续更新也将放在这里。从零开始构建工业自动化软件框架
在原定进行第二阶段"流程与状态机模块"开发前,我决定先对已完成的 Common 模块进行系统性单元测试,以确保各模块的可用性与稳定性。测试框架采用 NUnit 与 Moq。
测试
本轮测试主要覆盖以下模块:
- 配置构建与管理
- 各类序列化器(INI、JSON、XML)
- 日志工厂与日志输出
- IOC 容器注册与解析
Config测试
类配置器
首先是类配置器的测试。使用一个模拟类操作,最后比较类的内容与设置内容是否一致。
csharp
public class DummyConfig
{
public string Name { get; set; }
public int Speed { get; set; }
}
[TestFixture]
public class ConfigBuilderTest
{
/// <summary>
/// 测试是否可以正常build
/// </summary>
[Test]
public void BuildConfig_Test()
{
var builder = new ConfigBuilder();
builder.SetValue("Name", "MotorX")
.SetValue("Speed", 120);
var config = builder.BuildConfig<DummyConfig>();
Assert.That(config is DummyConfig);
Assert.That(config.Name, Is.EqualTo("MotorX"));
Assert.That(config.Speed, Is.EqualTo(120));
}
}
配置管理器工厂
然后是配置管理器工厂的测试,分为两个,一个是对序列化器工厂方法的测试,还有一个是对配置管理器工厂的测试。确保注入的路径和注入的序列化器是否符合预期。
csharp
[TestFixture]
public class ConfigManagerFactoryTest
{
private ConfigManagerFactory factory = new ConfigManagerFactory();
/// <summary>
/// 测试是否可以返回正确的序列化器
/// </summary>
/// <param name="type"></param>
/// <param name="expectedType"></param>
[TestCase(ConfigType.ini, typeof(IniConfigSerializer))]
[TestCase(ConfigType.json, typeof(JsonConfigSerializer))]
[TestCase(ConfigType.xml, typeof(XmlConfigSerializer))]
public void CreateSerializer_Test(ConfigType type, Type expectedType)
{
var serializer = factory.CreateSerializer(type);
Assert.That(serializer, Is.Not.Null);
Assert.That(serializer.GetType(), Is.EqualTo(expectedType));
}
/// <summary>
/// 测试是否可以正确抛出异常
/// </summary>
[Test]
public void CreateSerializer_Throw_Test()
{
Assert.Throws<NotSupportedException>(() => factory.CreateSerializer((ConfigType)999));
}
}
[TestFixture]
public class ConfigManagerFactory_CreateTests
{
private string testRoot;
private ConfigManagerFactory factory = new ConfigManagerFactory();
[SetUp]
public void Setup()
{
//创建测试路径
testRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(testRoot);
//设置测试路径
typeof(ConfigManagerFactory)
.GetField("configPath", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(factory, testRoot);
}
[TearDown]
public void Cleanup()
{
if (Directory.Exists(testRoot))
{
Directory.Delete(testRoot, true);
}
}
/// <summary>
/// 测试是否可以在正确的位置创建正确的文件
/// </summary>
/// <param name="type"></param>
/// <param name="expectedType"></param>
/// <param name="filetype"></param>
[TestCase(ConfigType.ini, typeof(IniConfigSerializer), "ini")]
[TestCase(ConfigType.json, typeof(JsonConfigSerializer), "json")]
[TestCase(ConfigType.xml, typeof(XmlConfigSerializer), "xml")]
public void CreateConfigManager_Test(ConfigType type, Type expectedType, string filetype)
{
var filename = "testfile";
var secondPath = "sub";
var expectedPath = Path.Combine(testRoot, secondPath, filename + "." + filetype);
var manager = factory.CreateConfigManager(type, filename, secondPath);
Assert.That(manager, Is.TypeOf<ConfigManager>());
var serializerField = typeof(ConfigManager)
.GetField("_serializer", BindingFlags.NonPublic | BindingFlags.Instance);
var pathField = typeof(ConfigManager)
.GetField("_path", BindingFlags.NonPublic | BindingFlags.Instance);
var serializer = serializerField.GetValue(manager);
var fullPath = pathField.GetValue(manager) as string;
Assert.That(serializer.GetType(), Is.EqualTo(expectedType));
Assert.That(fullPath, Is.EqualTo(expectedPath));
}
}
序列化器
然后就是各个序列化器的测试,确保可以正常进行序列化与反序列化。其中INI需要同时测试单实例和集合两种形式。
csharp
public class MotorTestClass
{
[IniConfigInstanceName]
public string Name { get; set; }
public int Speed { get; set; }
public bool Enabled { get; set; }
}
public class NoAttributeClass
{
public string Name { get; set; }
}
[TestFixture]
public class IniConfigSerializerTest
{
private IniConfigSerializer _serializer = new IniConfigSerializer();
/// <summary>
/// 测试是否可以正常序列化/反序列化单类
/// </summary>
[Test]
public void Serialize_Deserialize_Test()
{
var motor = new MotorTestClass() { Name = "MotorX", Speed = 100, Enabled = true };
var iniContent = _serializer.Serialize(motor);
var deserialized = _serializer.Deserialize<MotorTestClass>(iniContent);
Assert.That(deserialized.Name, Is.EqualTo("MotorX"));
Assert.That(deserialized.Speed, Is.EqualTo(100));
Assert.That(deserialized.Enabled, Is.EqualTo(true));
}
/// <summary>
/// 测试是否可以正常序列化/反序列化集合
/// </summary>
[Test]
public void Serialize_Deserialize_List_Test()
{
var listMotor = new List<MotorTestClass>()
{
new MotorTestClass() { Name = "MotorX", Speed = 100, Enabled = true },
new MotorTestClass() { Name = "MotorY", Speed = 200, Enabled = false },
new MotorTestClass() { Name = "MotorZ", Speed = 300, Enabled = true },
};
var iniContent = _serializer.Serialize(listMotor);
var deserialized = _serializer.Deserialize<List<MotorTestClass>>(iniContent);
Assert.That(typeof(IEnumerable).IsAssignableFrom(deserialized.GetType()));
Assert.That(deserialized.Count, Is.EqualTo(3));
Assert.That(deserialized[0].Name, Is.EqualTo("MotorX"));
Assert.That(deserialized[0].Speed, Is.EqualTo(100));
Assert.That(deserialized[0].Enabled, Is.EqualTo(true));
Assert.That(deserialized[1].Name, Is.EqualTo("MotorY"));
Assert.That(deserialized[1].Speed, Is.EqualTo(200));
Assert.That(deserialized[1].Enabled, Is.EqualTo(false));
Assert.That(deserialized[2].Name, Is.EqualTo("MotorZ"));
Assert.That(deserialized[2].Speed, Is.EqualTo(300));
Assert.That(deserialized[2].Enabled, Is.EqualTo(true));
}
/// <summary>
/// 测试是否可以正确抛出异常
/// </summary>
[Test]
public void Serialize_Deserialize_Throw_Test()
{
var noattr = new NoAttributeClass() { Name = "MotorX" };
var listnoattr = new List<NoAttributeClass>()
{
new NoAttributeClass() { Name = "MotorX" },
new NoAttributeClass() { Name = "MotorY" }
};
Assert.Throws<IniConfigException>(() => _serializer.Serialize(noattr));
Assert.Throws<IniConfigException>(() => _serializer.Serialize(listnoattr));
}
}
public class StepTestClass
{
public string Name { get; set; }
public int Delay { get; set; }
public bool Enabled { get; set; }
}
[TestFixture]
public class JsonConfigSerializerTest
{
private JsonConfigSerializer _serializer = new JsonConfigSerializer();
/// <summary>
/// 测试是否可以正常序列化/反序列化
/// </summary>
[Test]
public void Serialize_Deserialize_Test()
{
var step = new StepTestClass() { Name = "MotorMove", Delay = 10, Enabled = true };
var jsoncontent = _serializer.Serialize(step);
var deserialized = _serializer.Deserialize<StepTestClass>(jsoncontent);
Assert.That(deserialized.Name == "MotorMove");
Assert.That(deserialized.Delay == 10);
Assert.That(deserialized.Enabled == true);
}
/// <summary>
/// 测试是否可以正常抛出异常
/// </summary>
[Test]
public void Serialize_Deserialize_Throw_Test()
{
Assert.Throws<ConfigDeserializeException>(() => _serializer.Deserialize<StepTestClass>("123123"));
}
}
[TestFixture]
public class XmlConfigSerializerTest
{
private XmlConfigSerializer _serializer = new XmlConfigSerializer();
/// <summary>
/// 测试是否可以正常序列化/反序列化
/// </summary>
[Test]
public void Serialize_Deserialize_Test()
{
var step = new StepTestClass() { Name = "MotorMove", Delay = 10, Enabled = true };
var jsoncontent = _serializer.Serialize(step);
var deserialized = _serializer.Deserialize<StepTestClass>(jsoncontent);
Assert.That(deserialized.Name == "MotorMove");
Assert.That(deserialized.Delay == 10);
Assert.That(deserialized.Enabled == true);
}
/// <summary>
/// 测试是否可以正常抛出异常
/// </summary>
[Test]
public void Serialize_Deserialize_Throw_Test()
{
Assert.Throws<ConfigDeserializeException>(() => _serializer.Deserialize<StepTestClass>("123123"));
}
}
配置管理器
最后就是配置管理器的测试,确保可以正常对文件进行读取。
csharp
public class TestConfig
{
public string Name { get; set; }
}
[TestFixture]
public class ConfigManagerTest
{
private Mock<IConfigSerializer> _mockSerializer = new Mock<IConfigSerializer>();
private ConfigManager _manager;
private string _tempFile;
[SetUp]
public void SetUp()
{
_tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".ini");
_manager = new ConfigManager(_mockSerializer.Object, _tempFile);
}
[TearDown]
public void TearDown()
{
if (File.Exists(_tempFile)) File.Delete(_tempFile);
}
/// <summary>
/// 测试是否可以正确加载文本
/// </summary>
[Test]
public void LoadConfig_Test()
{
File.WriteAllText(_tempFile, "fake-content");
var expected = new TestConfig { Name = "Loaded" };
_mockSerializer.Setup(s => s.Deserialize<TestConfig>("fake-content")).Returns(expected);
var result = _manager.LoadConfig<TestConfig>();
Assert.That(result.Name, Is.EqualTo("Loaded"));
}
/// <summary>
/// 测试是否可以正确写入文本
/// </summary>
[Test]
public void SaveConfig_Test()
{
var testObj = new TestConfig { Name = "Test" };
_mockSerializer.Setup(s => s.Serialize(testObj)).Returns(testObj.Name);
_manager.SaveConfig(testObj);
Assert.That(File.ReadAllText(_tempFile), Is.EqualTo(testObj.Name));
}
}
Logger测试
日志工厂
日志工厂需要进行的测试包含可以正常创建日志,同名日志只生成一个实例,多日志缓存等功能。
csharp
[TestFixture]
public class LoggerFactoryTest
{
[TearDown]
public void TearDown()
{
var directoriesToDelete = new[]
{
"D:/SophonDATA/logs/test",
"D:/SophonDATA/logs_Debug/test",
"D:/SophonDATA/logs/test2",
"D:/SophonDATA/logs_Debug/test2"
};
foreach (var dir in directoriesToDelete)
{
if (Directory.Exists(dir))
{
Directory.Delete(dir, recursive: true);
}
}
}
/// <summary>
/// 测试是否能正确创建日志,是否同名只创建一个日志,是否能创建不同名称的日志
/// </summary>
[Test]
public void CreateLogger_Test()
{
var factory = new LoggerFactory(name => { return new Mock<ILoggerManager>().Object; });
var logger1 = factory.CreateLogger("test");
Assert.That(logger1, Is.Not.Null);
var logger2 = factory.CreateLogger("test");
Assert.That(logger1 == logger2);
var loggercacheField = factory.GetType()
.GetField("_loggercache", BindingFlags.NonPublic | BindingFlags.Instance);
var loggercache = loggercacheField
.GetValue(factory) as ConcurrentDictionary<string, ILoggerManager>;
Assert.That(loggercache.Count == 1);
var logger3 = factory.CreateLogger("test2");
loggercache = loggercacheField
.GetValue(factory) as ConcurrentDictionary<string, ILoggerManager>;
Assert.That(logger3, Is.Not.Null);
Assert.That(loggercache.Count == 2);
}
}
日志管理器
日志管理器则只需要测试是否可以正常写入日志即可。
csharp
[TestFixture]
public class NlogManagerTest
{
string directory = "D:/SophonDATA/logs/Test_Log";
string directory_debug = "D:/SophonDATA/logs_Debug/Test_Log";
[TearDown]
public void TearDown()
{
var directoriesToDelete = new[]
{
directory,
directory_debug
};
foreach (var dir in directoriesToDelete)
{
if (Directory.Exists(dir))
{
Directory.Delete(dir, recursive: true);
}
}
}
/// <summary>
/// 测试所有日志方法
/// </summary>
[Test]
public void Logger_Test()
{
string path = Path.Combine(directory, $"{DateTime.Now:yyyy-MM-dd}.log");
string path_debug = Path.Combine(directory_debug, $"{DateTime.Now:yyyy-MM-dd}.log");
NlogManager nlog = new NlogManager("Test_Log");
nlog.Trace("Test_Log_Trace");
nlog.Debug("Test_Log_Debug");
nlog.Info("Test_Log_Info");
nlog.Warn("Test_Log_Warn");
nlog.Error("Test_Log_Error");
nlog.Fatal("Test_Log_Fatal");
NLog.LogManager.Flush();
Assert.That(File.Exists(path));
Assert.That(File.Exists(path_debug));
}
}
IOC测试
容器测试包含了依赖注入的测试,注册和解析的正常运行,单例注册的解析等。
csharp
public class DITestClass
{
private readonly ILoggerFactory _loggerFactory;
public DITestClass(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public ILoggerManager GetLogger()
{
return _loggerFactory.CreateLogger("DITestClass");
}
}
[TestFixture]
public class DITest
{
string directory = "D:/SophonDATA/logs/DITestClass";
[TearDown]
public void TearDown()
{
var directoriesToDelete = new[]
{
"D:/SophonDATA/logs/DITestClass",
"D:/SophonDATA/logs_Debug/DITestClass"
};
foreach (var dir in directoriesToDelete)
{
if (Directory.Exists(dir))
{
Directory.Delete(dir, recursive: true);
}
}
}
[Test]
public void DI_Test()
{
string path = Path.Combine(directory, $"{DateTime.Now:yyyy-MM-dd}.log");
var builder = new ContainerBuilder();
builder.RegisterAllModuleExt();
builder.RegisterType<DITestClass>();
var container = builder.Build();
var testclass = container.Resolve<DITestClass>();
Assert.That(testclass, Is.Not.Null);
var logger = testclass.GetLogger();
logger.Info("DI test class write info log");
NLog.LogManager.Flush();
Assert.That(File.Exists(path));
}
}
[TestFixture]
public class ContainerRegisterTest
{
string directory = "D:/SophonDATA/logs/IOC_Test_Log";
[TearDown]
public void TearDown()
{
var directoriesToDelete = new[]
{
"D:/SophonDATA/logs/IOC_Test_Log",
"D:/SophonDATA/logs_Debug/IOC_Test_Log"
};
foreach (var dir in directoriesToDelete)
{
if (Directory.Exists(dir))
{
Directory.Delete(dir, recursive: true);
}
}
}
/// <summary>
/// 测试容器注册是否成功,resolve后是否能正常使用
/// </summary>
[Test]
public void Register_Test()
{
string path = Path.Combine(directory, $"{DateTime.Now:yyyy-MM-dd}.log");
var builder = new ContainerBuilder();
builder.RegisterAllModuleExt();
var container = builder.Build();
var loggerFactory = container.Resolve<ILoggerFactory>();
Assert.That(loggerFactory, Is.Not.Null);
Assert.That(loggerFactory, Is.InstanceOf<LoggerFactory>());
var loggerFactory1 = container.Resolve<ILoggerFactory>();
Assert.That(loggerFactory == loggerFactory1);
var logger = loggerFactory.CreateLogger("IOC_Test_Log");
Assert.That(logger, Is.Not.Null);
logger.Info("This is a test log from container");
NLog.LogManager.Flush();
Assert.That(File.Exists(path));
}
}
问题点发现
通过测试,也发现了一些问题,并且根据测试结果进行了bug的修复。
IniConfigInstanceName特性参数
测试过程中,发现该特性里面的参数是不需要的,直接使用特性标记的属性就可以直接获取到名称,用作Section的名字。
csharp
/// <summary>
/// 如果使用ini配置,则类需要使用此特性
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class IniConfigInstanceNameAttribute : Attribute
{
public IniConfigInstanceNameAttribute()
{
}
}
同时通过特性获取名称时,var props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) 约束也少写了一个。
csharp
/// <summary>
/// 通过特性获取实例名称
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
/// <exception cref="IniConfigException"></exception>
private string GetInstanceName(object obj)
{
var props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
var p = props.FirstOrDefault(x => x.GetCustomAttribute<IniConfigInstanceNameAttribute>() != null);
if (p == null)
{
throw new IniConfigException($"{obj.GetType().Name} 无法使用INI配置管理");
}
return p.GetValue(obj)?.ToString();
}
NLog配置文件
测试过程中,发现一直没法正常生成文件夹,排查后发现配置文件中的路径错误。
bash
<targets>
<target xsi:type="File" name="file"
fileName="D:/SophonDATA/logs/${logger}/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}" />
<target xsi:type="File" name="debugfile"
fileName="D:/SophonDATA/logs_debug/${logger}/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}" />
</targets>
日志工厂
测试过程中,发现每次第一个日志写入是会失败的,查看nlog-internal.log发现是创建文件夹时意外退出了,后来进行排查,发现NLog创建路径时是会出现异常的,所以我直接把创建文件夹放在了日志工厂获取日志时。这样再次测试,问题不再出现。
csharp
/// <summary>
/// 工厂创建日志
/// 如果cache有,则直接给出,如果没有,则调用传入的委托,并把新的放入cache
/// </summary>
/// <param name="loggername"></param>
/// <returns></returns>
public ILoggerManager CreateLogger(string loggername)
{
string path = $"D:/SophonDATA/logs/{loggername}/";
string path_debug = $"D:/SophonDATA/logs_debug/{loggername}/";
Directory.CreateDirectory(path);
Directory.CreateDirectory(path_debug);
return _loggercache.GetOrAdd(loggername, _loggerCreator);
}
总结
本轮测试暴露并修复了多个隐藏问题,包括配置特性误用、NLog 文件夹创建失败等,进一步增强了代码的健壮性。完整源码已上传至 GitHub,欢迎大家前往围观指正。Sophon_Github地址