如何从 VS2013 为基于 ADO.Net 的存储库编写单元测试代码

How to write unit test code for my ADO.Net based repository from VS2013

我是单元测试领域的新手。所以请指导我如何在我的控制器中为基于 ADO.Net 的存储库和操作方法编写单元测试?

我将使用 VS 自己的单元测试框架。

所以请看代码告诉我有哪些类和函数需要通过单元测试?

如果有人告诉我哪些区域需要进行单元测试,哪些区域不需要进行单元测试,那将非常有帮助?

如果可能,请写一些单元测试代码,告诉我如何为操作、学生存储库和学生视图模型编写单元测试

我是否需要使用 mock 或 vs 自己的单元测试 是否足以测试我的存储库功能?

这是我的基本存储库

namespace DataLayer.Repository
{
   public abstract class AdoRepository<T> where T : class
    {
        private SqlConnection _connection;
        public virtual void Status(bool IsError, string strErrMsg)
        {

        }

        public AdoRepository(string connectionString)
        {
            _connection = new SqlConnection(connectionString);
        }

        public virtual T PopulateRecord(SqlDataReader reader)
        {
            return null;
        }

        public virtual void GetDataCount(int count)
        {

        }

        protected IEnumerable<T> GetRecords(SqlCommand command)
        {
            var reader = (SqlDataReader) null;
            var list = new List<T>();
            try
            {
                command.Connection = _connection;
                _connection.Open();
                reader = command.ExecuteReader();
                while (reader.Read())
                {
                    list.Add(PopulateRecord(reader));
                }

                reader.NextResult();
                if (reader.HasRows)
                {
                    while (reader.Read())
                    {
                        GetDataCount(Convert.ToInt32(reader["Count"].ToString()));
                    }
                }
                Status(false, "");
            }
            catch (Exception ex)
            {
                Status(true, ex.Message);
            }
            finally
            {
                // Always call Close when done reading.
                reader.Close();
                _connection.Close();
                _connection.Dispose();
            }

            return list;
        }

        protected T GetRecord(SqlCommand command)
        {
            var reader = (SqlDataReader)null;
            T record = null;

            try
            {
                command.Connection = _connection;
                _connection.Open();

                reader = command.ExecuteReader();
                while (reader.Read())
                {
                    record = PopulateRecord(reader);
                    Status(false, "");
                    break;
                }
            }
            catch (Exception ex)
            {
                Status(true, ex.Message);
            }
            finally
            {
                reader.Close();
                _connection.Close();
                _connection.Dispose();
            }
            return record;
        }

        protected IEnumerable<T> ExecuteStoredProc(SqlCommand command, string CountColName="TotalCount")
        {
            var reader = (SqlDataReader)null;
            var list = new List<T>();

            try
            {
                command.Connection = _connection;
                command.CommandType = CommandType.StoredProcedure;
                _connection.Open();
                reader = command.ExecuteReader();

                while (reader.Read())
                {
                    var record = PopulateRecord(reader);
                    if (record != null) list.Add(record);
                }

                reader.NextResult();
                if (reader.HasRows)
                {
                    while (reader.Read())
                    {
                        GetDataCount(Convert.ToInt32(reader[CountColName].ToString()));
                    }
                }

            }
            finally
            {
                // Always call Close when done reading.
                reader.Close();
                _connection.Close();
                _connection.Dispose();
            }
            return list;
        }
    }
}

这是我的学生资料库

public class StudentRepository : AdoRepository<Student>
    {
        public int DataCounter { get; set; }
        public bool hasError { get; set; }
        public string ErrorMessage { get; set; }

        public StudentRepository(string connectionString)
            : base(connectionString)
        {
        }

        public IEnumerable<Student> GetAll()
        {
            // DBAs across the country are having strokes 
            //  over this next command!
            using (var command = new SqlCommand("SELECT ID, FirstName,LastName,IsActive,StateName,CityName FROM vwListStudents"))
            {
                return GetRecords(command);
            }
        }
        public Student GetById(string id)
        {
            // PARAMETERIZED QUERIES!
            using (var command = new SqlCommand("SELECT ID, FirstName,LastName,IsActive,StateName,CityName FROM vwListStudents WHERE Id = @id"))
            {
                command.Parameters.Add(new ObjectParameter("id", id));
                return GetRecord(command);
            }
        }

        public IEnumerable<Student> SaveXML(string strXML, int PageNo,int PageSize,string SortCol,string SortOrder)
        {
            if (PageNo <= 0) PageNo = 1;

            using (var command = new SqlCommand("USP_SaveStudent"))
            {
                command.Parameters.Add("@Data", SqlDbType.VarChar,-1).Value = strXML;
                command.Parameters.Add("@PageNbr", SqlDbType.Int).Value = PageNo;
                command.Parameters.Add("@PageSize", SqlDbType.Int).Value = PageSize;
                command.Parameters.Add("@SortColumn", SqlDbType.VarChar, 20).Value = SortCol;
                command.Parameters.Add("@SortOrder", SqlDbType.VarChar, 4).Value = SortOrder;

                return ExecuteStoredProc(command);
            }
        }

        public IEnumerable<Student> Delete(int id, int PageNo, int PageSize, string SortCol, string SortOrder)
        {
            if (PageNo <= 0) PageNo = 1;

            using (var command = new SqlCommand("USP_DeleteStudent"))
            {
                command.Parameters.Add("@ID", SqlDbType.VarChar, -1).Value = id;
                command.Parameters.Add("@PageNbr", SqlDbType.Int).Value = PageNo;
                command.Parameters.Add("@PageSize", SqlDbType.Int).Value = PageSize;
                command.Parameters.Add("@SortColumn", SqlDbType.VarChar, 20).Value = SortCol;
                command.Parameters.Add("@SortOrder", SqlDbType.VarChar, 4).Value = SortOrder;

                return ExecuteStoredProc(command);
            }
        }

        public IEnumerable<Student> GetStudents(int PageNo, int PageSize, string SortCol, string SortOrder)
        {
            //string strSQL = "SELECT * FROM vwListStudents WHERE ID >=" + StartIndex + " AND ID <=" + EndIndex;
            //strSQL += " ORDER BY " + sortCol + " " + sortOrder;
            //strSQL += ";SELECT COUNT(*) AS Count FROM vwListStudents";
            //var command = new SqlCommand(strSQL);
            //return GetRecords(command);
            if (SortOrder == "Ascending")
                SortOrder = "ASC";
            else if (SortOrder == "Descending")
                SortOrder = "DESC";

                if (PageNo <= 0) PageNo = 1;

            using (var command = new SqlCommand("USP_GetStudentData"))
            {
                command.Parameters.Add("@PageNbr", SqlDbType.Int).Value = PageNo;
                command.Parameters.Add("@PageSize",SqlDbType.Int).Value=  PageSize;
                command.Parameters.Add("@SortColumn", SqlDbType.VarChar, 20).Value = SortCol;
                command.Parameters.Add("@SortOrder", SqlDbType.VarChar, 4).Value = SortOrder;
                return ExecuteStoredProc(command);
            }
        }

        public override Student PopulateRecord(SqlDataReader reader)
        {
            return new Student
            {
                ID = Convert.ToInt32(reader["ID"].ToString()),
                FirstName = reader["FirstName"].ToString(),
                LastName = reader["LastName"].ToString(),
                IsActive = Convert.ToBoolean(reader["IsActive"]),
                StateID = Convert.ToInt32(reader["StateID"].ToString()),
                StateName = reader["StateName"].ToString(),
                CityID = Convert.ToInt32(reader["CityID"].ToString()),
                CityName = reader["CityName"].ToString()
            };
        }

        public override void GetDataCount(int count)
        {
            DataCounter = count;
        }

        public override void Status(bool IsError, string strErrMsg)
        {
            hasError = IsError;
            ErrorMessage = strErrMsg;
        }
    }

Student Model
---------------
public class Student
    {
        public int ID { get; set; }
        [Required(ErrorMessage = "First Name Required")]
        public string FirstName { get; set; }
        [Required(ErrorMessage = "Last Name Required")]
        public string LastName { get; set; }
        public bool IsActive { get; set; }

        public int StateID { get; set; }
        public string StateName { get; set; }

        public int CityID { get; set; }
        public string CityName { get; set; }
}

Student View Model
------------------
public class StudentListViewModel
        {
            public int StartIndex { get; set; }
            public int EndIndex { get; set; }
            public int page { get; set; }
            public int RowCount { get; set; }
            public int PageSize { get; set; }
            public int CurrentPage { get; set; }

            public string sort { get; set; }
            public string sortdir { get; set; }

            public IList<Student> Students { get; set; }

            [Required(ErrorMessage = "State Required")]
            public int SelectedStateId { set; get; }
            public IList<State> States { get; set; }

            [Required(ErrorMessage = "City Required")]
            public int SelectedCityId { set; get; }
            public IList<City> Cities { get; set; }

            public StudentListViewModel()
            {
                PageSize = 5;
                sort = "ID";
                sortdir = "ASC";
                CurrentPage = 1;
            }

            public void SetUpParams(StudentListViewModel oSVm)
            {
                if (oSVm.page == 0)
                    oSVm.page = 1;

                StartIndex = ((oSVm.page * oSVm.PageSize) - oSVm.PageSize) + 1;
                EndIndex = (oSVm.page * oSVm.PageSize);
                CurrentPage = (StartIndex - 1) / oSVm.PageSize;

                if (string.IsNullOrEmpty(oSVm.sort))
                    oSVm.sort = "ID";

                if (string.IsNullOrEmpty(oSVm.sortdir))
                    oSVm.sortdir = "ASC";
            }
}

Here is few action which i need to test
---------------------------------------
public ActionResult List(StudentListViewModel oSVm)
        {
            if (Request.IsAjaxRequest())
                System.Threading.Thread.Sleep(1000); // just simulate delay of one second

            StudentListViewModel SVm = new StudentListViewModel();
            SVm.SetUpParams(oSVm);
            SVm.Students = _Studentdata.GetStudents(oSVm.page, oSVm.PageSize, oSVm.sort, oSVm.sortdir).ToList();
            SVm.States = _Statedata.GetAll().ToList();
            SVm.Cities = _Citydata.GetAll().ToList();
            SVm.RowCount = _Studentdata.DataCounter;
            return View("ListStudents",SVm);
        }

        [HttpPost]
        public ActionResult UpdateStudents(StudentListViewModel oSVm, string Action)
        {
            if (Request.IsAjaxRequest())
                System.Threading.Thread.Sleep(1000); // just simulate delay of one second

            StudentListViewModel SVm = new StudentListViewModel();
            SVm.SetUpParams(oSVm);
            if (Action == "UPDATE")
            {
                SVm.Students = _Studentdata.SaveXML(new List<Student>(oSVm.Students).ToXml("Students"),
                    oSVm.page, oSVm.PageSize, oSVm.sort, oSVm.sortdir).ToList();
            }
            else if (Action == "DELETE")
            {
                SVm.Students = _Studentdata.Delete(oSVm.Students[0].ID,
                    oSVm.page, oSVm.PageSize, oSVm.sort, oSVm.sortdir).ToList();

            }

            SVm.States = _Statedata.GetAll().ToList();
            SVm.Cities = _Citydata.GetAll().ToList();
            SVm.RowCount = _Studentdata.DataCounter;
            return PartialView("_StudentGrid", SVm);
        }

        [HttpPost]
        public ActionResult RefreshStudents(StudentListViewModel oSVm)
        {
            if (Request.IsAjaxRequest())
                System.Threading.Thread.Sleep(1000); // just simulate delay of one second

            StudentListViewModel SVm = new StudentListViewModel();
            SVm.SetUpParams(oSVm);
            SVm.Students = _Studentdata.GetStudents(oSVm.page, oSVm.PageSize, oSVm.sort, oSVm.sortdir).ToList();
            SVm.States = _Statedata.GetAll().ToList();
            SVm.Cities = _Citydata.GetAll().ToList();
            SVm.RowCount = _Studentdata.DataCounter;
            return PartialView("_StudentGrid", SVm);
        }

        [HttpGet]
        public JsonResult GetCityName(int StateID)
        {
            if (Request.IsAjaxRequest())
                System.Threading.Thread.Sleep(1000); // just simulate delay of one second

            return Json(new {CityList =_Citydata.GetCityByStateId(StateID)} , JsonRequestBehavior.AllowGet);
    }

谢谢

一般准则:

如果您对超出内存块的方法进行单元测试(例如,连接到数据库或写入磁盘),您不想对它们进行单元测试。

这是有道理的。例如,如果您正在测试是否可以在您的 people table 中保存另一个 Person 对象,但测试失败了,这是哪里出了问题?我是数据库连接吗?数据库是否已满?也许它因为一些奇怪的原因而停止了?还是您的代码中存在错误? - 问题是,你不能确定,单元测试是为了让你确定哪里出了问题。

所以:策略 1: 停止编写在写入磁盘或数据库的同时执行任何形式的 IF 测试的代码。这些 classes 应该只使用契约,这样你就可以完全模拟数据库层。例如:

public class PeopleManager{

  private IDataWriter<Person> _personWriter;

  public PeopleManager(IDataWriter<Person> personWriter){
    _personWriter = personWriter;
  }

  public void ImportantMethodToTest(Person person){
    if(..important conditionsTested...){
      _personWriter.Update(person);
    }
  }
}

上面的 class 是 unit-testable,因为它不依赖于 IDataWriter 的契约以外的任何东西,你可以在 class 中实现你的写作到数据库,和零逻辑。它只是写。

测试现在超快且超简单:

[TestMethod]
public void ImportantMethod_PersonIsNull_NothingIsWrittenToDatabase(){

  // Arrange
  var writerMock = new Mock<IDataWriter<Person>>();
  var peopleManager = new PeopleManager(writerMock.Object);

  // Act
  peopleManager.ImportantMethodToTest(null);

  // Assert
  writerMock.Verify( writer => writer.Update(It.IsAny<Person>), Times.Never());


}

希望你能看到图片。需要搜索以了解更多信息的是 SOLID 原则,我在这里使用的部分是单一责任原则和依赖倒置原则。这些原则使您的代码可以进行单元测试

[TestMethod]
public void List_StudentListViewModelIsValid_ViewModelIsPopulated()
{
    // Arrange
    var students = Builder<Student>().CreateListOfSize(10).Build();
    var repositoryMock = new Mock<StudentRepository>();

    repositoryMock.Setup(r => r.GetAll()).Returns(students);

    var testInstance = new StudentListViewModel(repositoryMock.Object); // DIP

    // Act
    testInstance.List()

    // ASsert
    // Check your values etc
}

这里要注意的重要一点是我对存储库所做的事情。我没有在 List 方法中使用 NEW,而是在 viewModel 构造函数中注入了存储库。你不应该在这样的函数内部使用 "new" 。相反,使用它们通过 class 的构造函数传递这些对象。这就是众所周知的 依赖倒置原则 并且是使您的代码可测试的面向对象编程的 SOLID 原则之一。

因此您必须重写您的视图模型以将存储库作为构造函数参数。这样,当你进行单元测试时,你可以给 viewmodel 一个 mock 作为存储库,而不必使用 new。

同样,您的抽象存储库 class 可以像任何接口一样被模拟。

编辑: Builder 代码来自一个名为 NBuilder 的 nuget 包,每当我只需要来自模拟的一些数据时,我通常会使用它。当我在测试代码中需要它们时,它可以让我构建各种大小的列表:)