从零开始构建工业自动化软件框架:基础框架搭建(三)容器、配置、日志功能测试

前言

文章同步在公众号与知乎更新,账号全部同名。搜索"小黄花呀小黄花"即可找到我。

前面已经基本完成了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地址

相关推荐
CodeCraft Studio2 小时前
图像处理控件Aspose.Imaging教程:使用 C# 将 SVG 转换为 EMF
图像处理·microsoft·c#·svg·aspose·图片格式转换·emf
★YUI★3 小时前
学习游戏制作记录(将各种属性应用于战斗以及实体的死亡)8.5
学习·游戏·unity·c#
jason成都3 小时前
ubuntu编译opendds开发(C#)
linux·ubuntu·c#·opendds
小黄花呀小黄花5 小时前
从零开始构建工业自动化软件框架:基础框架搭建(一)容器与日志功能实现
c#
马达加斯加D6 小时前
C# --- 本地缓存失效形成缓存击穿触发限流
开发语言·缓存·c#
q__y__L8 小时前
C# WaitHandle类的几个有用的函数
java·开发语言·c#
步、步、为营8 小时前
.NET8 正式发布, C#12 新变化
ui·c#·.net
伽蓝_游戏9 小时前
Unity UI的未来之路:从UGUI到UI Toolkit的架构演进与特性剖析(7)
游戏·ui·unity·架构·c#·游戏引擎·.net
q__y__L14 小时前
C#线程同步(三)线程安全
安全·性能优化·c#