使用私有构造函数对 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 字符串和创建实例的帮助程序代码。
我正在尝试测试一个只有私有构造函数的 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 字符串和创建实例的帮助程序代码。