Need guidance to do Nunit test case for data insert scenario in .NET

为君一笑 提交于 2019-12-20 04:32:21

问题


I have the below Employee model class and console client.

Employee class:-

public class Employee
{
    public int EmployeeId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public int phoneNumber { get; set; }

    public Employee()
    {

    }

    public Employee(string fname,string lname,int age,int phone)
    {            
        this.FirstName = fname;
        this.LastName = lname;
        this.Age = age;
        this.phoneNumber = phone;
    }

    public void InsertEmployee()
    {
        SqlConnection con = new SqlConnection("sqlconnection");
        SqlCommand cmd = new SqlCommand("sp_insert", con);
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.AddWithValue("fname", this.FirstName);
        cmd.Parameters.AddWithValue("lname", this.LastName);
        cmd.Parameters.AddWithValue("age", this.Age);
        cmd.Parameters.AddWithValue("phoneno",this.phoneNumber);            
        con.Open();
        cmd.ExecuteNonQuery();            
        con.Close();
    }

    public List<Employee> GetAllEmployees()
    {
        SqlConnection connection = new SqlConnection("sqlconnection");
        SqlCommand cmd = new SqlCommand("GetAllEmployees", connection);
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        connection.Open();
        SqlDataReader dr = cmd.ExecuteReader();
        List<Employee> employeeList = new List<Employee>();         
        while (dr.Read())
        {
            Employee emp = new Employee();
            emp.EmployeeId = int.Parse(dr["empID"].ToString());
            emp.FirstName = dr["fname"].ToString();
            emp.LastName = dr["lname"].ToString();
            emp.Age= int.Parse(dr["age"].ToString());
            emp.phoneNumber= int.Parse(dr["phone"].ToString());
            employeeList.Add(emp);
        }
        return employeeList;
    }
}

******Client code****

class Program
{
    static void Main(string[] args)
    {
        Employee newEmp = new Employee("Ram", "Prem", 30, 90000007);
        newEmp.InsertEmployee();
        List<Employee> empList = newEmp.GetAllEmployees();
    }
}

********************

The above code works and it is fine.

Now I was told to write Nunit test method for Insert method and fetch method.

How can I write NUnit test method for Insert with following conditions:-
1) How to ensure the what ever value supplied got inserted into database.There should not be manual validation.It should be part of Nunit test.
2) In case if new column got introduced in the table .

In the Employee model City property got added and City param is passed as parameter.

Lets say new column City Nullable column added to the table and in the Insert stored procedure the developer didnt add the new column in the insert statement but the City param is added in the Procedure.

In this above scenario how Nunit test will identify this bug (that is City is not inserted into the table?

How to write Nunit test method to test with above conditions?


回答1:


First of all, you would need to make use of the Repository Pattern so you can perform Unit Test in your code.

Create an interface IEmployeeRepository which will define the operations that you want to perform related to the Employees:

public interface IEmployeeRepository {

  void Insert(Employee employee);
  List<Employee> GetAll();

}

Now create the EmployeeRepository class which must inherit from that interface, and implement the functions that you explicitly defined:

public class EmployeeRepository : IEmployeeRepository {

  public void Insert(Employee employee){
    // Your code to insert an employee from the db.
  }

  public List<Employee> GetAll(){
    // Your code to get all the employees from the db.
  }

}

So in this way, the Employee class is where you just define the properties that match your Employee table in the db and the constructor:

public class Employee {

        public int EmployeeId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public int phoneNumber { get; set; }

        public Employee()
        {

        }

        public Employee(string fname,string lname,int age,int phone)
        {            
            this.FirstName = fname;
            this.LastName = lname;
            this.Age = age;
            this.phoneNumber = phone;
        }
}

Unit Testing

How to ensure the what ever value supplied got inserted into database.There should not be manual validation.It should be part of Nunit test.

You really don't want to hit the database and perform any operations. So, you should Mock the calls to the database.

using System.Collections.Generic;
using NUnit.Framework;
using Moq;

namespace UnitTestProject1
{
    [TestFixture]
    public class EmployeeRepositoryTests
    {
        [Test]
        public void GetAll()
        {
            // Arrange
            var repositoryMock = new Mock<IEmployeeRepository>();
            var employees = new List<Employee>
            {
                new Employee("John", "Smith", 20, 12345678),
                new Employee("Robert", "Taylor", 20, 12345678)
            };

            // We simulate the DB returns the previous employees when calling the "GetAll()" method 
            repositoryMock.Setup(x => x.GetAll()).Returns(employees);

            // Act
            var result = repositoryMock.Object.GetAll();

            // Assert
            CollectionAssert.AreEquivalent(employees, result);
        }
    }
}



回答2:


Employee class is too tightly coupled to implementation concerns as it is directly calling SqlConnection and related implementations.

A previous answer suggested the Repository Pattern, which would be the standard way of dealing with this issue.

But based on your comment to that answer.

Our application is developed with such design that model should have Insert,update and delete methods. These methods are part of the Employee class. We can't redesign it now.

I get the impression you are unable to change to a more flexible design based on requirements. That does not mean that you cannot still keep the current structure and still make the code testable. This would however require refactoring the Employee class to be dependent on abstractions and separate its concerns.

Create the abstraction of your data access.

public interface IEmployeeRepository {
    void Add(Employee model);
    List<Employee> GetAll();
}

This will be used in the Employee class to make calls to persistence as was done before but with the flexibility of being able to use different implementations.

Here is the refactored Employee class after applying separation of concerns.

public class Employee {
    IEmployeeRepository repository;

    public int EmployeeId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public int phoneNumber { get; set; }

    public Employee() {

    }

    public Employee(string fname, string lname, int age, int phone) {
        this.FirstName = fname;
        this.LastName = lname;
        this.Age = age;
        this.phoneNumber = phone;
    }

    public void InsertEmployee() {
        repository.Add(this);
    }

    public List<Employee> GetAllEmployees() {
        return repository.GetAll();
    }

    public void SetRepository(IEmployeeRepository repository) {
        this.repository = repository;
    }
}

Note that the previous exposed API for this class has not changed but the responsibilities of the class has be inverted with the inclusion of the abstraction.

Given that you are using what looks like an Active Record Pattern, which heavily favor encapsulation to the point where testing without a database is quite difficult. Thus favoring integration tests more so than isolated unit tests.

Since constructor injections does not fit well into your current design, I suggest exposing a method that would allow for the injection of the dependency into the record.

This is only being suggested because of the stated restrictions on the class. It violates encapsulation as it hides preconditions for proper usage.

With that in place the Employee class can now be tested in isolation, using mocked implementations of its dependencies injected when arranging the test.

[Test]
public void InsertEmployee_Should_Add_Record() {
    //Arrange
    var employees = new List<Employee>();

    var repositoryMock = new Mock<IEmployeeRepository>();

    repositoryMock
        .Setup(_ => _.Add(It.IsAny<Employee>()))
        .Callback<Employee>(employees.Add)
        .Verifiable();

    var newEmp = new Employee("Ram", "Prem", 30, 90000007);
    newEmp.SetRepository(repositoryMock.Object);

    //Act
    newEmp.InsertEmployee();

    //Assert
    employees.Should()
        .HaveCount(1)
        .And
        .Contain(newEmp);
    repositoryMock.Verify();
}

[Test]
public void GetAllEmployees_Should_GetAll() {
    //Arrange
    var expected = new List<Employee>() {
        new Employee("Ram", "Prem", 30, 90000007),
        new Employee("Pam", "Rem", 31, 90000008)
    };

    var repositoryMock = new Mock<IEmployeeRepository>();

    repositoryMock
        .Setup(_ => _.GetAll())
        .Returns(expected)
        .Verifiable();

    var newEmp = new Employee();
    newEmp.SetRepository(repositoryMock.Object);

    //Act
    var actual = newEmp.GetAllEmployees();

    //Assert
    expected.Should().Equal(actual);
    repositoryMock.Verify();
}

The production implementation of the repository can also be improved through separation of concerns by not depending on implementation concerns.

Here are examples of the interfaces and supporting classes that can be used.

public interface IDbConnectionFactory {
    ///<summary>
    /// Creates a connection based on the given connection string.
    ///</summary>
    IDbConnection CreateConnection(string nameOrConnectionString);
}

public class SqlConnectionFactory : IDbConnectionFactory {
    public IDbConnection CreateConnection(string nameOrConnectionString) {
        return new SqlConnection(nameOrConnectionString);
    }
}

public static class DbExtension {
    public static IDbDataParameter AddParameterWithValue(this IDbCommand command, string parameterName, object value) { 
        var parameter = command.CreateParameter();
        parameter.ParameterName = parameterName;
        parameter.Value = value;
        command.Parameters.Add(parameter);
        return parameter;
    }

    public static IDbCommand CreateCommand(this IDbConnection connection, string commandText) {
        var command = connection.CreateCommand();
        command.CommandText = commandText;
        return command;
    }
}

public class EmployeeSqlRepository : IEmployeeRepository {
    private IDbConnectionFactory connectionFactory;

    public EmployeeSqlRepository(IDbConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    public void Add(Employee model) {
        using (var connection = connectionFactory.CreateConnection("sqlconnection")) {
            using (var command = connection.CreateCommand("sp_insert")) {
                command.CommandType = CommandType.StoredProcedure;
                command.AddParameterWithValue("fname", model.FirstName);
                command.AddParameterWithValue("lname", model.LastName);
                command.AddParameterWithValue("age", model.Age);
                command.AddParameterWithValue("phoneno", model.phoneNumber);
                connection.Open();
                command.ExecuteNonQuery();
                connection.Close();
            }
        }
    }

    public List<Employee> GetAll() {
        var employeeList = new List<Employee>();
        using (var connection = connectionFactory.CreateConnection("sqlconnection")) {
            using (var command = connection.CreateCommand("GetAllEmployees")) {
                command.CommandType = CommandType.StoredProcedure;
                connection.Open();
                using (var reader = command.ExecuteReader()) {
                    while (reader.Read()) {
                        var employee = new Employee();
                        employee.EmployeeId = int.Parse(reader["empID"].ToString());
                        employee.FirstName = reader["fname"].ToString();
                        employee.LastName = reader["lname"].ToString();
                        employee.Age = int.Parse(reader["age"].ToString());
                        employee.phoneNumber = int.Parse(reader["phone"].ToString());

                        employee.SetRepository(this);

                        employeeList.Add(employee);
                    }
                }
            }
        }
        return employeeList;
    }
}


来源:https://stackoverflow.com/questions/42448751/need-guidance-to-do-nunit-test-case-for-data-insert-scenario-in-net

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!