我必须对此进行单元测试,这使我发疯,因为我不知道如何在不连接数据库的情况下执行此操作。这不是我自己编写的代码,但代码构成了我的成员组。
public IActionResult FamilyDoctor()
{
List<Patient> GetPatientData()
{
List<Patient> PatientDataArray = new List<Patient>();
Connection conn = new Connection();
MySqlConnection connection = null;
try
{
connection = new MySqlConnection(conn.getConnectionString());
connection.Open();
MySqlCommand cmd = new MySqlCommand();
cmd.Connection = connection;
cmd.CommandText = "SELECT * FROM measurement;";
cmd.Prepare();
MySqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
int ID = reader.GetInt32("id");
decimal bloodsugar = reader.GetDecimal("bloodsugar");
decimal bloodsugardesired = reader.GetDecimal("bloodsugardesired");
string description = reader.GetString("description");
DateTime time = reader.GetDateTime("time");
//int status = reader.GetInt16("status");
PatientDataArray.Add(new Patient(ID, bloodsugar, bloodsugardesired, description, time));
}
}
finally
{
if (connection != null)
connection.Close();
}
return PatientDataArray;
}
ViewData["Patient"] = GetPatientData();
return View();
}
}
这样的代码不能进行单元测试,只能对已知数据状态进行集成测试。 EF不会帮助您。要编写可单元测试的代码,您需要开始执行关注点分离和控制反转。例如,存储库模式可以通过包装数据检索来提供帮助,以便业务逻辑可以接收对存储库的依赖关系,单元测试可以模拟该依赖关系。该存储库是使用EF还是ADO与所测试的代码无关。
就是说,您发布的上述代码没有任何业务逻辑,它只是从数据库返回所有患者数据。单元测试旨在测试业务逻辑,该逻辑将基于检索到的数据执行某些操作。但是,您可以用它代替上面的代码作为最基本的示例:
public class PatientRepository : IPatientRepository
{
public IEnumerable<Patient> GetAllPatients()
{
List<Patient> PatientDataArray = new List<Patient>();
Connection conn = new Connection();
MySqlConnection connection = null;
try
{
connection = new MySqlConnection(conn.getConnectionString());
connection.Open();
MySqlCommand cmd = new MySqlCommand();
cmd.Connection = connection;
cmd.CommandText = "SELECT * FROM measurement;";
cmd.Prepare();
MySqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
int ID = reader.GetInt32("id");
decimal bloodsugar = reader.GetDecimal("bloodsugar");
decimal bloodsugardesired = reader.GetDecimal("bloodsugardesired");
string description = reader.GetString("description");
DateTime time = reader.GetDateTime("time");
PatientDataArray.Add(new Patient(ID, bloodsugar, bloodsugardesired, description, time));
}
}
finally
{
if (connection != null)
connection.Close();
}
return PatientDataArray;
}
然后在控制器中:
public PatientController(IPatientRepository patientRepository)
{
this.PatientRepository = patientRepository;
}
public IActionResult FamilyDoctor()
{
ViewData["Patient"] = PatientRepository.GetAllPatients();
return View();
}
您可能会说,这在某种程度上是没有意义的测试,但是该视图无需数据库即可进行测试,因为可以模拟PatientRepository并将其设置为返回已知数据状态或引发异常等,而无需数据库连接。如果控制器正在检查或转换患者数据,则可以根据已知状态测试该逻辑。您无需对存储库代码进行单元测试,它只是在获取数据。您对测试行为进行单元测试,在这种情况下,实际上没有行为。
这可以帮助使代码可测试,但是无论有没有存储库,它都不是特别有效。您的数据访问方法正在数据库中执行SELECT *
来填充模型。实体框架和工作单元模式可以帮助提高效率,同时仍然易于测试。
例如,您定义一个实体以反映完整的Measurement和系统中的相关记录。使用存储库模式,您返回实体的IQueryable
,被测调用代码可以使用这些实体。
控制器定义了工作单元,用作DbContext的容器。 (想想DbContext之类的Connection)
public PatientController(IUnitOfWorkFactory unitOfWorkFactory, IPatientRepository patientRepository)
{
this.UnitOfWorkFactory = unitOfWorkFactory;
this.PatientRepository = patientRepository;
}
public IActionResult FamilyDoctor()
{
using(var unitOfWork = UnitOfWorkFactory.Create())
{
var measurements = PatientRepository.GetAllMeasurements(unitOfWork);
var viewModels = measurements.Select(x => new MeasurementViewModel
{
ID = x.Id,
BloodSugar = x.BloodSugar,
BloodSugarDesired = x.BloodSugarDesired,
Description = x.Description,
Time = x.Time
}).ToList();
ViewData["Patient"] = viewModels;
return View();
}
现在上面的工作单位工厂/模式只是一个存根示例。您可以搜索有关在何处/如何实现工作单元模式的各种示例。 UoW的目的是包装DbContext。它用作EF的DBContext的外观,可以更轻松地替换为要测试的模拟。
使用EF的测量库看起来更像:
public IQueryable<Measurement> GetAllMeasurements(IUnitOfWork unitOfWork)
{
return unitOfWork.Context.Measurements.AsQueryable();
}
注意:如果只想将AsQueryable()
整体返回IQueryable
,则需要DbSet
。您在Where
条件下应用的所有过滤都会自动返回IQueryable
。在许多情况下,您将对数据使用低级规则,例如可能是IsActive状态,以反映活动的,未删除/隐藏的患者的测量结果。存储库可以强制执行这些基本级别的规则,因此您无需记住到处检查它们:
public IQueryable<Patient> GetAllPatients(IUnitOfWork unitOfWork)
{
return unitOfWork.Context.Measurements.Where(x => x.IsActive && x.Patient.IsActive);
}
这将仅返回活动患者的活动测量值。对于更多特定于场景的条件,可以在调用中添加方法或参数,以使存储库应用其他过滤,但是我更愿意让使用者根据需要应用特定条件。它使存储库更简单,更轻巧,更易于模拟。
这给了我们什么?
通过返回IQueryable<Measurement>
,我们允许存储库的使用者选择他们想要的数据,或他们想要对该数据进行什么处理。在上述情况下,通过仅选择5个字段,执行的SQL将仅选择这5列,而不是SELECT *
。我们应用的所有Where
条件都将转换为SQL,从而加快查询速度。如果我们只想要Count
或使用Any
进行存在检查,则与选择所有数据进行检查相比,这些查询将导致执行性能更好。
该代码对于单元测试而言要简单得多,因为我们的测试夹具可以构建IUnitOfWorkFactory和存储库的模拟,其中存储库模拟简单地将由患者组成的列表或数组作为.AsQueryable()
返回,以供逻辑使用。
同样,您只想从数据库中获取一组数据并将其吐出到视图中的情况,实际上并不能从单元测试中获得任何价值。它根本什么也没做。但是,如果您有想要执行某项操作的代码,该代码将在其中取回数据并根据返回的数据来决定要做什么或是否要做某事,请重构代码以从中进行数据检索业务逻辑,并利用EF来有效地查询该数据将变得更加有用和易于测试。
接下来是查看如何返回数据。单元测试非常适合通过检查返回值和声明模拟来声明行为的结果。使用像ViewData
这样的ASP.Net特定结构,很难进行测试。返回View(viewModels)
会容易一些。通常,我建议您设计页面以异步加载数据,而“ View”方法不采用填充的模型,而是使用GET调用将JSon()
作为ActionResult返回,将其呈现为空并启动数据加载,或者执行到服务,该服务返回适用数据的集合,而控制器仅包装这些数据。在最后一种情况下,您将对服务而不是控制器进行单元测试。
因此,考虑到这一点,您可以采用现有代码并对其进行单元测试吗?不,不是。但这应该给您一些弹药,让您回到团队的其他成员那里,并说,如果他们希望单元测试涵盖代码,这是他们应采用的重构方法。单元测试是必须预先容纳的。如果项目未使用IoC / DI等易于测试的模式,则可能很难在开发过程的后期引入。