使用私有构造函数对 Class 进行单元测试

Unit Testing a Class With A Private Constructor

我正在尝试测试一个只有私有构造函数的 class。这是一个课程注册系统。这些课程不是通过我们的应用程序创建的,因此我们有意没有 public 构造函数。相反,我们使用 EF 获取数据库中已有的课程,并将学生注册到这些课程。

我正在尝试测试课程 class 的注册方法,但是我无法创建实例。我可以用 course = (Course)Activator.CreateInstance(typeof(Course), true);,但是我没有办法设置必要的属性,因为它们是私有的。

在没有构造函数的情况下进行单元测试的推荐方法是什么?

这是代码的精简版。

public class Course
{
    private Course()
    {
    }

    public int Id { get; private set; }
    public string Name { get; private set; }
    public bool Open { get; private set; }
    public virtual ICollection<Student> Students { get; private set; }

    public void Register(string studentName)
    {
        if (Open)
        {
            var student = new Student(studentName);
            Students.Add(student);
        }
    }
}

// 用法 //

using (var db = new SchoolContext())
        {
            var course = db.Courses.Include(x => x.Students).Where(x => x.Name == courseName).First();

            course.Register(studentName);
            db.SaveChanges();
        }

// 单元测试 //

[Fact]
public void CanRegisterStudentForOpenClass(){
    // HERE I HAVE NO WAY TO CHANGE THE OPEN VARIABLE
    var course = (Course)Activator.CreateInstance(typeof(Course), true);
    course.Register("Bob");
}



    

是的,您可以使用反射。你的代码已经在那里了;

您可以使用 typeof(Course).GetProperty("PropertyName") 获取类型的属性和字段,然后您可以使用 SetValue 设置所需的值,并首先将要修改的实例作为参数传递,然后再传递值。 在你的情况下 true;

注意:在您的示例中,如果您的 Open 为真,您还需要添加学生集合。

这里有一个工作示例:

[Fact]
public void CanRegisterStudentForOpenClass()
{
    var course = (Course)Activator.CreateInstance(typeof(Course), true);

    typeof(Course).GetProperty("Open").SetValue(course, true, null);
    ICollection<Student> students = new List<Student>();
    typeof(Course).GetProperty("Students").SetValue(course, students, null);

    course.Register("Bob");

    Assert.Single(course.Students);
}

如果您不想使用反射,那么我建议您使用 internal 类(而不是 private)并在您的实现程序集上使用 InternalsVisibleToAttribute

你可以find more about the attribute here。这是有关如何使用它的快速指南!

第 1 步。将此属性添加到要测试其内部代码的程序集中。

[assembly: InternalsVisibleToAttribute("MyUnitTestedProject.UnitTests")]

步骤 2. 将 private 更改为 internal

public class Course
{
    internal Course()
    {
    }

    public int Id { get; internal set; }
    public string Name { get; internal set; }
    public bool Open { get; internal set; }
    public virtual ICollection<Student> Students { get; internal set; }

    /* ... */
}

第 3 步。像往常一样编写测试!

[Fact]
public void CanRegisterStudentForOpenClass()
{
    var course = new Course();
    course.Id = "#####";
    course.Register("Bob");
}

您可以使用 TypeMock Isolator 或 JustMock(均已付费)或使用 MS Fakes(仅在 VS Enterprise 中可用)来伪造私有构造函数和成员。

还有一个免费的 Pose 库,可让您伪造对属性的访问。

不幸的是,无法伪造私有构造函数。因此,您需要使用反射创建 class 的实例。


添加package.
打开命名空间:

using Pose;

测试代码:

[Fact]
public void CanRegisterStudentForOpenClass()
{
    var course = (Course)Activator.CreateInstance(typeof(Course), true);

    ICollection<Student> students = new List<Student>();
    Shim studentsPropShim = Shim.Replace(() => Is.A<Course>().Students)
                                .With((Course _) => students);

    Shim openPropShim = Shim.Replace(() => Is.A<Course>().Open)
                            .With((Course _) => true);

    int actual = 0;

    PoseContext.Isolate(() =>
    {
        course.Register("Bob");
        actual = course.Students.Count;
    },
    studentsPropShim, openPropShim);

    Assert.Equal(1, actual);
}

正如一些人在这里提到的,单元测试某些东西 private 要么是代码味道,要么是你正在编写的标志 the wrong tests

在这种情况下,如果您使用的是 Core,则您想要做的是使用 EF 的 in-memory 数据库,或者使用 EF6 进行模拟。

对于 EF6 您可以按照文档进行操作 here

我想说的是与其在你做的地方更新你的 dbContext,不如通过依赖注入传递它。如果这超出了你正在做的工作范围,(我假设这是实际的课程作业,所以去 DI 可能有点矫枉过正)那么你可以创建一个包装器 class 它需要一个 dbcontext 并在地点。

对调用此代码的位置采取一些自由...


class Semester
{
  //...skipping members etc

  //if your original is like this
  public RegisterCourses(Student student)
  {
    using (var db = new SchoolContext())
    {
      RegisterCourses(student, db);
    }
  }

  //change it to this
  public RegisterCourses(Student student, SchoolContext db)
  {
    var course = db.Courses.Include(x => x.Students).Where(x => x.Name == courseName).First();

      course.Register(studentName);
      db.SaveChanges();
  }
}  

[Fact]
public void CanRegisterStudentForOpenClass()
{
  //following after https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking#testing-query-scenarios
  var mockCourseSet = new Mock<DbSet<Course>>();
  mockCourseSet.As<IQueryable<Course>>().Setup(m => m.Provider).Returns(data.Provider);
  mockCourseSet.As<IQueryable<Course>>().Setup(m => m.Expression).Returns(data.Expression);
  mockCourseSet.As<IQueryable<Course>>().Setup(m => m.ElementType).Returns(data.ElementType);
  mockCourseSet.As<IQueryable<Course>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
  //create an aditional mock for the Student dbset
  mockStudentSet.As.........

  var mockContext = new Mock<SchoolContext>();
  mockContext.Setup(c => c.Courses).Returns(mockCourseSet.Object);
  //same for student so we can include it

  mockContext.Include(It.IsAny<string>()).Returns(mockStudentSet); //you can change the isAny here to check for Bob or such

  
  var student = Institution.GetStudent("Bob");
  var semester = Institution.GetSemester(Semester.One);
  semester.RegisterCourses(student, mockContext);
}

如果您使用的是 EFCore,则可以从 here

开始关注它

您可以创建默认实例的 JSON 表示并使用 Newtonsoft 对其进行反序列化。

像这样:

using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using privateConstructor;

namespace privateConstructorTest
{
    [TestClass]
    public class CourseTest
    {
        [TestMethod]
        public void Register_WhenOpenIsTrue_EnableAddStudents()
        {
            // Arrange
            const string json = @"{'Id': 1, 'name':'My Course', 'open':'true', 'students':[]}";
            var course = CreateInstance<Course>(json);

            // Act
            course.Register("Bob");

            // Assert
            Assert.AreEqual(1, course.Students.Count);
        }

        [TestMethod]
        public void Register_WhenOpenIsFalse_DisableAddStudents()
        {
            // Arrange
            const string json = @"{'Id': 1, 'name':'My Course', 'open':'false', 'students':[]}";
            var course = CreateInstance<Course>(json);

            // Act
            course.Register("Bob");

            // Assert
            Assert.AreEqual(0, course.Students.Count);
        }

        private static T CreateInstance<T>(string json) =>
            JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
            {
                ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
                ContractResolver = new ContractResolverWithPrivates()
            });

        public class ContractResolverWithPrivates : CamelCasePropertyNamesContractResolver
        {
            protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
            {
                var prop = base.CreateProperty(member, memberSerialization);
                if (prop.Writable) return prop;
                var property = member as PropertyInfo;
                if (property == null) return prop;
                var hasPrivateSetter = property.GetSetMethod(true) != null;
                prop.Writable = hasPrivateSetter;

                return prop;
            }
        }
    }
}

为了进行更清晰的测试 class,您可以提取 JSON 字符串和创建实例的帮助程序代码。