Skip to content

unit testing.cs

文件信息

  • 📄 原文件:01_unit_testing.cs
  • 🔤 语言:csharp

完整代码

csharp
// ============================================================
//                      单元测试
// ============================================================
// C# 主流测试框架:xUnit(推荐)、NUnit、MSTest
// xUnit:[Fact] 单个测试,[Theory] 参数化测试
// FluentAssertions:更可读的断言语法
// Moq:Mock 对象,隔离外部依赖
//
// 安装依赖:
//   dotnet add package xunit
//   dotnet add package xunit.runner.visualstudio
//   dotnet add package FluentAssertions
//   dotnet add package Moq
// 运行测试:dotnet test

using System;
using System.Collections.Generic;
using System.Linq;

// ============================================================
//                      被测试的生产代码
// ============================================================

// ----------------------------------------------------------
// 1. 简单计算器(用于演示基本单元测试)
// ----------------------------------------------------------
class Calculator
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;
    public int Multiply(int a, int b) => a * b;

    public double Divide(double a, double b)
    {
        if (b == 0)
            throw new DivideByZeroException("除数不能为零");
        return a / b;
    }

    public double Power(double base_, double exponent) => Math.Pow(base_, exponent);
}

// ----------------------------------------------------------
// 2. 字符串工具(用于演示边界条件测试)
// ----------------------------------------------------------
class StringUtils
{
    public string Reverse(string s)
    {
        if (s is null) throw new ArgumentNullException(nameof(s));
        return new string(s.Reverse().ToArray());
    }

    public bool IsPalindrome(string s)
    {
        if (s is null) throw new ArgumentNullException(nameof(s));
        s = s.ToLower();
        int left = 0, right = s.Length - 1;
        while (left < right)
        {
            if (s[left] != s[right]) return false;
            left++;
            right--;
        }
        return true;
    }

    public string[] SplitWords(string sentence)
    {
        if (string.IsNullOrWhiteSpace(sentence))
            return Array.Empty<string>();
        return sentence.Split(' ', StringSplitOptions.RemoveEmptyEntries);
    }
}

// ----------------------------------------------------------
// 3. 订单服务(演示依赖注入和 Mock)
// ----------------------------------------------------------
interface IProductRepository
{
    decimal GetPrice(string productId);
    bool Exists(string productId);
}

interface IOrderLogger
{
    void LogOrder(string orderId, decimal total);
}

class OrderService
{
    private readonly IProductRepository _repository;
    private readonly IOrderLogger _logger;

    public OrderService(IProductRepository repository, IOrderLogger logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public decimal CalculateTotal(Dictionary<string, int> items)
    {
        decimal total = 0;
        foreach (var (productId, quantity) in items)
        {
            if (!_repository.Exists(productId))
                throw new ArgumentException($"商品 {productId} 不存在");
            total += _repository.GetPrice(productId) * quantity;
        }
        return total;
    }

    public string PlaceOrder(Dictionary<string, int> items)
    {
        decimal total = CalculateTotal(items);
        string orderId = $"ORD-{DateTime.UtcNow:yyyyMMddHHmmss}";
        _logger.LogOrder(orderId, total);
        return orderId;
    }
}

// ============================================================
//                      测试代码演示
// ============================================================
// 注意:以下是模拟 xUnit 测试框架的演示写法
// 实际测试需要独立的 Test 项目和 xUnit/NUnit 框架

class TestRunner
{
    // 简单的测试结果记录
    private static int _passed = 0;
    private static int _failed = 0;
    private static List<string> _failures = new();

    static void Assert(bool condition, string testName)
    {
        if (condition)
        {
            _passed++;
            Console.WriteLine($"  ✓ {testName}");
        }
        else
        {
            _failed++;
            _failures.Add(testName);
            Console.WriteLine($"  ✗ {testName}");
        }
    }

    static void AssertEqual<T>(T expected, T actual, string testName)
    {
        bool condition = EqualityComparer<T>.Default.Equals(expected, actual);
        if (condition)
        {
            _passed++;
            Console.WriteLine($"  ✓ {testName}");
        }
        else
        {
            _failed++;
            _failures.Add($"{testName} (期望: {expected}, 实际: {actual})");
            Console.WriteLine($"  ✗ {testName} (期望: {expected}, 实际: {actual})");
        }
    }

    static void AssertThrows<TException>(Action action, string testName) where TException : Exception
    {
        try
        {
            action();
            _failed++;
            _failures.Add($"{testName} (未抛出 {typeof(TException).Name})");
            Console.WriteLine($"  ✗ {testName} (未抛出预期异常)");
        }
        catch (TException)
        {
            _passed++;
            Console.WriteLine($"  ✓ {testName}");
        }
        catch (Exception ex)
        {
            _failed++;
            _failures.Add($"{testName} (抛出了错误类型 {ex.GetType().Name})");
            Console.WriteLine($"  ✗ {testName} (错误异常类型)");
        }
    }

    static void Main()
    {
        Console.WriteLine("=== 单元测试演示 ===\n");

        // ----------------------------------------------------------
        // Calculator 测试
        // ----------------------------------------------------------
        Console.WriteLine("Calculator 测试:");
        var calc = new Calculator();

        AssertEqual(8, calc.Add(3, 5), "Add(3,5) = 8");
        AssertEqual(0, calc.Add(0, 0), "Add(0,0) = 0");
        AssertEqual(-2, calc.Add(-5, 3), "Add(-5,3) = -2");
        AssertEqual(5, calc.Subtract(8, 3), "Subtract(8,3) = 5");
        AssertEqual(6, calc.Multiply(2, 3), "Multiply(2,3) = 6");
        AssertEqual(0, calc.Multiply(0, 100), "Multiply(0,100) = 0");

        double divResult = calc.Divide(10, 4);
        Assert(Math.Abs(divResult - 2.5) < 1e-10, "Divide(10,4) = 2.5");

        AssertThrows<DivideByZeroException>(() => calc.Divide(5, 0), "Divide by zero throws");

        // 参数化测试(等价于 [Theory] [InlineData])
        Console.WriteLine("\n  参数化测试 - 加法:");
        var addTestCases = new[] { (1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, -50, 50) };
        foreach (var (a, b, expected) in addTestCases)
        {
            AssertEqual(expected, calc.Add(a, b), $"Add({a},{b}) = {expected}");
        }

        // ----------------------------------------------------------
        // StringUtils 测试
        // ----------------------------------------------------------
        Console.WriteLine("\nStringUtils 测试:");
        var utils = new StringUtils();

        AssertEqual("olleH", utils.Reverse("Hello"), "Reverse('Hello')");
        AssertEqual("", utils.Reverse(""), "Reverse('')");
        AssertEqual("a", utils.Reverse("a"), "Reverse('a')");

        Assert(utils.IsPalindrome("racecar"), "IsPalindrome('racecar')");
        Assert(utils.IsPalindrome("A"), "IsPalindrome('A')");
        Assert(!utils.IsPalindrome("hello"), "!IsPalindrome('hello')");
        Assert(utils.IsPalindrome(""), "IsPalindrome('')");

        AssertThrows<ArgumentNullException>(() => utils.Reverse(null!), "Reverse(null) throws");

        // SplitWords
        string[] words = utils.SplitWords("Hello World C#");
        AssertEqual(3, words.Length, "SplitWords 返回3个词");
        AssertEqual("Hello", words[0], "SplitWords[0] = 'Hello'");

        string[] empty = utils.SplitWords("  ");
        AssertEqual(0, empty.Length, "SplitWords('  ') = 空数组");

        // ----------------------------------------------------------
        // 测试隔离(Mock 模式)
        // ----------------------------------------------------------
        Console.WriteLine("\nOrderService 测试(使用测试替身):");

        // 用测试替身替代真实依赖
        var mockRepo = new MockProductRepository();
        var mockLogger = new MockOrderLogger();
        var orderService = new OrderService(mockRepo, mockLogger);

        // 场景1:正常下单
        var items = new Dictionary<string, int> { { "P001", 2 }, { "P002", 1 } };
        decimal total = orderService.CalculateTotal(items);
        AssertEqual(299.0m, total, "订单总价 = 299 元");

        string orderId = orderService.PlaceOrder(items);
        Assert(orderId.StartsWith("ORD-"), "订单ID格式正确");
        AssertEqual(1, mockLogger.LoggedOrders.Count, "日志记录了1条订单");
        AssertEqual(299.0m, mockLogger.LoggedOrders[0].Total, "日志金额正确");

        // 场景2:商品不存在
        var badItems = new Dictionary<string, int> { { "INVALID", 1 } };
        AssertThrows<ArgumentException>(
            () => orderService.CalculateTotal(badItems),
            "无效商品抛出 ArgumentException");

        // ----------------------------------------------------------
        // 测试结果汇总
        // ----------------------------------------------------------
        Console.WriteLine("\n" + new string('=', 50));
        Console.WriteLine($"测试结果: {_passed} 通过 / {_failed} 失败");
        if (_failures.Any())
        {
            Console.WriteLine("失败详情:");
            foreach (var f in _failures)
                Console.WriteLine($"  - {f}");
        }
        else
        {
            Console.WriteLine("所有测试通过!");
        }

        // ----------------------------------------------------------
        // xUnit 实际写法示例(注释形式展示)
        // ----------------------------------------------------------
        Console.WriteLine("\n=== xUnit 实际写法 ===");
        Console.WriteLine("""
            // 测试类
            public class CalculatorTests
            {
                private readonly Calculator _calc = new();

                [Fact]
                public void Add_TwoPositiveNumbers_ReturnsSum()
                {
                    var result = _calc.Add(3, 5);
                    result.Should().Be(8);  // FluentAssertions
                }

                [Theory]
                [InlineData(1, 2, 3)]
                [InlineData(-1, 1, 0)]
                [InlineData(0, 0, 0)]
                public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
                {
                    _calc.Add(a, b).Should().Be(expected);
                }

                [Fact]
                public void Divide_ByZero_ThrowsDivideByZeroException()
                {
                    Action act = () => _calc.Divide(10, 0);
                    act.Should().Throw<DivideByZeroException>();
                }
            }
            """);
    }
}

// ----------------------------------------------------------
// 测试替身(Test Doubles)
// ----------------------------------------------------------
class MockProductRepository : IProductRepository
{
    private readonly Dictionary<string, decimal> _prices = new()
    {
        { "P001", 99.50m },
        { "P002", 100.00m },
        { "P003", 49.99m },
    };

    public decimal GetPrice(string productId) =>
        _prices.TryGetValue(productId, out decimal price) ? price : 0;

    public bool Exists(string productId) => _prices.ContainsKey(productId);
}

class MockOrderLogger : IOrderLogger
{
    public List<(string OrderId, decimal Total)> LoggedOrders { get; } = new();

    public void LogOrder(string orderId, decimal total)
    {
        LoggedOrders.Add((orderId, total));
    }
}

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布