如何组织这些测试以鼓励代码重用

本文关键字:代码 何组织 测试 | 更新日期: 2023-09-27 17:51:12

总结

正在努力组织我的单元测试,我需要一些指导来优化我的努力。

遵循单元测试

的最佳实践,抽象类应通过其派生类型进行测试,从而在我的单元测试中构建与我的领域模型中相同的继承层次结构。

例外,某些测试是多余的,并且测试代码通过此层次结构成倍增加。

例如,属性测试最终通过执行几乎相同的测试和编写相同的代码行来结束。

您将如何组织测试?

我认为无论您测试什么,有些测试都是相同的。其中一些是:

  • 测试属性是否在分配时返回预期值;
  • 测试字符串属性在赋值为 null 时是否引发;
  • 测试字符串属性在分配的字符串比允许的长字符串时是否引发;
  • 测试在分配超出范围的值时是否引发整数属性;
  • 测试传入 null 参数时方法是否引发;

尽管上述示例与属性相关,但对于方法和可能想要测试的任何其他方法也是如此。

因此,这些基本测试可以属于其他测试类可以从中继承的SuperTestBaseClass,并从目标测试成员的基中调用测试方法。

一些例子

AuditableEntity

public abstract class AuditableEntity {
    protected AuditableEntity() { }
    DateTime CreatedAt { get; set; }
    string CreatedBy { get; set; }
    DateTime DeletedAt { get; set; }
    string DeletedBy { get; set; }
    int Id { get; protected set; }
    DateTime UpdatedAt { get; set; }
    string UpdatedBy { get; set; }
}

Customer

public class Customer : AuditableEntity {
    public class Customer() : base() { Invoices = new Collection<Invoice>(); }
    public string Name { get; set; }
    public IEnumerable<Invoice> Invoices { get; private set; }
    public long PhoneNumber { get; set; }
}

Invoice

public class Invoice : AuditableEntity {
    public class Invoice() : base() { Items = new Collection<Item>(); }
    public IEnumerable Items { get; private set; }
    public double GrandTotal { get { return Items.Sum<Item>(i => i.Price); } }
}

SuperTestBaseClass

public abstract class SuperTestBaseClass {
    protected SuperTestBaseClass() { }
    protected void Throws<TException>(Action<T> action) {
        // arrange
        Type expected = typeof(TException);
        Exception actual = null;
        // act
        try { action; } catch (Exception ex) { actual = ex; }
        // assert
        Assert.IsInstanceOfType(actual, expected);
    }
    protected void PropertyGetSetValue(Action<T> action, T value) {
        // arrange
        T expected = value;
        action; // assign the value to the property, let's say
        // act
        T actual = action; // gets the value out of the property, let's say
        // assert
        Assert.AreEqual(expected, actual);
    }
}

AuditableEntityTests<T>

public abstract class AuditableEntityTests<T> where T : IAuditableEntity : SuperTestBaseClass {
    [TestMethod]
    public CreatedAt_ReturnsNowByDefault() {
        // arrange
        DateTime expected = DateTime.Now;
        // act
        DateTime actual = Entity.CreatedAt;
        // assert
        Assert.AreEqual(expected, actual);  
    }        
    [TestMethod]
    public void CreatedBy_ThrowsArgumentNullExceptionWhenNullOrWhiteSpace() {            
        Throws<ArgumentNullException>(Entity.CreatedBy = null);
    }
    protected T Entity { get; set; }
}

CustomerTests

[TestClass]
public class CustomerTests : AuditableEntityTests<Customer> {
    public CustomerTests() : base() { }
    [TestMethod]
    public void Name_GetSetValue() { 
        PropertyGetSetValue(customer.Name, RandomValues.RandomString());
    }
    [TestMethod]
    public void Name_CannotBeNull() {
        Throws<ArgumentNullException>(Customer.Name = null);
    }
    [ClassInitialize]
    public void CustomerEntitySetUp() { Entity = customer; }
    [TestInitialize]
    public void CustomerSetUp() { customer = new Customer(); }
    private Customer customer;
}

以及其他一些基本问题

虽然这些问题可以提出一些其他好问题,但我在这里问它们,因为我希望回答以这个背景所说明的情况为导向。

  • 如何在组织中以Func<T, TResult>方式使用它?
  • 如何在组织中以我想使用它的方式使用Action<T>

从这两个问题中,我希望我能让它们完全按照我作为委托传入的参数进行操作。

最后,

  • 你认为以这种方式组织测试是糟糕的吗?
  • 实际上,SuperTestBaseClass可以属于一个类库,以便通过多个项目使用代码。

如何组织这些测试以鼓励代码重用

通过继承重新组织测试是可行的,尽管与类图中使用的继承类型不同。

[TestClass]
public abstract class AuditableEntityTests {
    protected AuditableEntityTests(IAuditableEntity entity) { Entity = entity; }
    protected IAuditableEntity Entity { get; set; }
    public abstract void GetsAndSetsValue();
    public virtual Throws<TException>(Action action) { 
        // arrange
        Type expected = typeof(TException);
        Exception actual = null;
        // act
        try { action(); } catch(Exception ex) { actual = ex; }
        // assert
        Assert.IsInstanceOfType(actual, expected);
    }
    [TestClass]
    public class CreatedAt : AuditableEntityTests {
        public CreatedAt() : base(new Customer()) { }
        [TestMethod]
        public void GetsAndSetsValue() {
            // arrange
            DateTime expected = DateTime.Now;
            Entity.CreatedAt = expected;
            // act
            DateTime actual = Entity.CreatedAt;
            // assert
            Assert.AreEqual(expected, actual);
        }
    }
    [TestClass]
    public class CreatedBy : AutditableEntityTests {
        public CreatedBy() : base(new Customer());
        [TestMethod]
        public void GetsAndSetsValue() {
            // arrange
            string expected = RandomValues.RandomString();
            Entity.CreatedBy = expected;
            // act
            string actual = Entity.CreatedBy;
            // assert
            Assert.AreEqual(expected, actual);
        }
        [TestMethod]
        public void CannotBeNull() {
            // arrange 
            string unexpected = null;
            Entity.CreatedBy = RandomValues.RandomString();
            // act
            Entity.CreatedBy = unexpected;
            string actual = Entity.CreatedBy;
            // assert
            Assert.IsNotNull(actual);
        }
        [TestMethod]
        public void ThrowsWhenLongerThan12() {
            // arrange
            int length = 256;
            string tooLong = RandomValues.RandomString(length);
            // act
            Action action = () => { Entity.CreatedBy = tooLong; };
            // assert
            Throws<ArgumentOutOfRangeException>(action);
        }
    }
}

这不仅鼓励代码重用,而且还以易于管理的方式在 TestExplorer 中组织测试。