【问题标题】:How to write unit test code for my ADO.Net based repository from VS2013如何从 VS2013 为我的基于 ADO.Net 的存储库编写单元测试代码
【发布时间】:2016-03-21 17:08:55
【问题描述】:

我是单元测试领域的新手。那么请指导我如何在我的控制器中为基于 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);
    }

谢谢

【问题讨论】:

标签: c# unit-testing asp.net-mvc-5 repository-pattern


【解决方案1】:
[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 构造函数中注入了存储库。你不应该在这样的函数中使用“新”。相反,通过使用它们的类的构造函数传递这些对象。这被称为依赖倒置原则,是面向对象编程的 SOLID 原则之一,它使您的代码可测试。

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

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

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

【讨论】:

  • 这一行在做什么 `repositoryMock.Setup(r => r.GetAll()).Returns(students);` ?
  • 以上行是否返回学生列表?
  • Builder 可以随机生成学生对象。如果我需要 1000 名学生参加单元测试,构建者可以为我制作。对于你的第二个问题,我告诉模拟,如果调用 GetAll() 方法,它应该返回我使用 Builder 制作的学生列表
  • var testInstance = new StudentListViewModel(repositoryMock.Object); // DIP 这一行有什么用? repositoryMock.Object 是什么?
  • 这是我创建我正在测试的类的实例的地方。如果我想对 StudentListViewModel 进行单元测试,我需要先创建一个 :) 模拟本身并不是抽象类的实现。但是模拟包含一个对象属性,它实现了它。 RepositoryMock.Object 是 mock 创建的东西,它实现了 StudentRepository。
【解决方案2】:

一般准则:

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

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

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

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);
    }
  }
}

上面的类是可单元测试的,因为它不依赖于除了 IDataWriter 的契约之外的任何东西,你可以在一个写入数据库的类和零逻辑中实现它。它只是写。

现在测试这个超级快速和超级简单:

[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 原则,我在这里使用的部分是单一责任原则和依赖倒置原则。这些原则使您的代码可以进行单元测试

【讨论】:

  • 对我自己的小评论 - 您仍然希望运行针对磁盘/数据库的测试,但只是偶尔,即每晚一次,甚至更少。您还需要在每次要运行测试时加载已知状态的数据库,这是大量的工作
  • 模拟可以与抽象类一起使用吗?如果你看到我的代码然后明白我使用抽象基础存储库类。我没有使用界面。那么你对那些不使用界面的人有什么建议。他们如何测试他们的数据库交互类?
  • 模拟抽象类与模拟接口几乎相同。你不应该遇到任何问题
  • 你能发布一个示例,比如如何测试扩展抽象类的类的功能。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-01
  • 2020-06-25
  • 2013-06-02
  • 1970-01-01
  • 2021-08-08
  • 1970-01-01
  • 2019-07-10
相关资源
最近更新 更多