如何解决使用私有构造器对类进行单元测试
我正在尝试测试仅具有私有构造函数的类。这是用于课程注册系统的。这些课程无法通过我们的应用程序创建,因此我们故意没有公共构造函数。相反,我们使用EF来获取数据库中已经存在的课程,并为他们注册学生。
我正在尝试测试Course类的register方法,但是无法创建实例。我可以用
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");
}
,
正如一些人在这里提到的,对private
进行单元测试要么是代码味道,要么是您正在编写的符号the wrong tests。
在这种情况下,如果要使用Core或使用EF6进行模拟,则要使用EF的内存数据库。
对于EF6,您可以关注文档here
我要说的不是通过您的操作来更新dbContext,而是通过Dependency Injection将其传递。如果这超出了您正在做的工作的范围(我假设这是实际的课程,那么去DI可能就算过头了),那么您可以创建一个使用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
开始使用 ,您可以使用TypeMock Isolator或JustMock(均已付费)或使用MS Fakes(仅在VS Enterprise中提供)来伪造私有构造函数和成员。
还有一个免费的Pose库,可让您伪造对属性的访问。
不幸的是,私有构造函数无法伪造。因此,您将需要使用反射创建该类的实例。
添加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);
}
,
您可以创建默认实例的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,'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;
}
}
}
}
为了拥有更简洁的测试类,您可以提取JSON字符串和用于创建实例的帮助程序代码。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。