Unit Testing with .NET Tools

    The main idea of ​​unit (or unit, as it is also called) testing is testing individual program components, i.e. classes and their methods. It is very useful to develop code that is covered with tests, because when used correctly, the possibility of regression in the history of the development of the program is practically eliminated - “they added something new, half of the old one has come down”. The TDD development methodology, Test Driven Development, is also very fashionable now. According to her, the programmer first develops a set of tests for future functionality, calculates all the execution options, and only then begins to write directly working code suitable for the tests already written.

    Since the existence of tests in the program is not only a confirmation of the developer’s qualifications, but also often the requirement of the customer, I decided to address this issue and “touch” the tests close.

    I work mainly in Visual Studio, I write on sharpe, which means that the choice was almost limited to two products - Nunit and Unit Testing Framework.

    Unit Testing Framework is a built-in testing system in Visual Studio developed by Microsoft that is constantly evolving (among the latest updates is the ability to test UI, which was already written on the Habré), and most importantly, it will almost certainly exist all the time while there is Visual Studio, which is not the case with the latest developments. Excellent integration in the IDE and the function of calculating the percentage of code coverage in the program finally tipped the scales - the choice was made.
    The network contains a rather large number of various testing tutorials, but all of them usually come down to testing a written calculator or comparing strings. These things, of course, are also necessary and important, but they don’t pull on serious examples, frankly, they don’t. I myself can test such tasks even in my mind.

    Here is a list of more serious tasks
    • checking the correctness of creating the database
    • checking the correctness of the business logic
    • getting the benefit of all this (in my case, the benefit was received))

    So, let's get started!

    DB testing

    According to the principles of testing and sound logic, we will have a separate base for testing. Therefore, we create the TestDB database, add the Users table to it
    CREATE TABLE [dbo].[Users](
      [id] [int] IDENTITY(1,1) NOT NULL,
      [login] [nchar](100) COLLATE Ukrainian_CI_AS NOT NULL,
      [hash] [nchar](50) COLLATE Ukrainian_CI_AS NOT NULL,
      [level] [int] NULL,
    CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
    ([id] ASC)
    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON))
    CREATE UNIQUE NONCLUSTERED INDEX [IX_UserName] ON [dbo].[Users]
    ([Login] ASC)
    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)


    and enter data into it
    INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'Sirix', N'827ccb0eea8a706c4c34a16891f84e7b', 1)
    INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'Tester', N'3095C3E4F1465133E5E6BE134EB2EBE2', 1)
    INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'Admin', N'E3AFED0047B08059D0FADA10F400C1E5', 2)
    INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'U1', N'827CCB0EEA8A706C4C34A16891F84E7B', 1)

    Add a stored procedure, which we will test. She will be engaged in a rather familiar thing - to return the user ID for a given login and password hash.
    CREATE PROCEDURE [dbo].[GetUserId]
      (
      @login nchar(100),
      @hash nchar(50),
      @id int OUTPUT
      )
    AS
    SELECT @id = ID FROM Users WHERE Login = @login AND hash = @hash
      RETURN

    * This source code was highlighted with Source Code Highlighter .

    Unit Testing Framework allows you to test various aspects of the database - checking the schema, the number of records in tables, stored procedures, query execution time, their results and much more.

    Start tests

    Create a new solution like TestProject. Let's call it LinkCatalog.Tests. And we add a new Database Unit Test to it. A window for setting up a database connection appears. Set up a connection to our database and click OK. In the same window, you can specify the parameters of data auto-generation for tests. This function uses Data Generation Plan and allows you to populate the database table with test values, using patterns and even regular expressions. Click OK and get to the database testing window.




    Test number 1

    The first test is the simplest. Check the number of entries in the table:
    Select count(*) FROM Users

    Now we establish the condition for the test to be correct. As I said, there is a large list of all kinds of criteria, but we are interested in one of the simplest ones - ScalarValue. All condition options are configured in the Properties window. All! The first test is done. We start and watch. What was required to prove - the lines are successfully stored in the database.








    Test number 2

    Now it is time to do a more real check than the number of records. This is a stored procedure. What if the developers of the database made a critical mistake in it? But this is the most important part of the user authentication subsystem!
    Create a new database test by clicking on the green plus sign Here is the test code:




    /* Выходное значение */
    DECLARE @id INT;
    SET @id = -1

    /* Должно установить id = 1 */
    EXEC GetUserId N'Sirix', N'827ccb0eea8a706c4c34a16891f84e7b', @id OUTPUT;

    SELECT * FROM Users WHERE id = @id

    * This source code was highlighted with Source Code Highlighter.

    Another condition is already used here - it is expected that there will be exactly 1 row in the selection. This test also succeeds, which is good news. According to the results of testing the database, we can say that it works stably and as expected. Now, the coverage of database procedures reaches 100% - the limit that is not easy to achieve in a more complex application or database with more tables / procedures / relationships.






    Code Unit Tests

    Now let's start testing the code.
    I had a small ASP.NET application with an implementation of the MVC pattern. The model was a separate assembly under the proud name of DAL and included a wrapper for access to the database. One of the requirements was the use of DataReader in ADO.NET. Settings, as is customary, are stored in web.config.
    I decided to start testing the model: this code was to be tested
    namespace LinkCatalog.DAL
    {
      public class UserModel
      {
    ...........
    public static int GetUserIdByName(string username)
        {
          string query = "SELECT ID FROM Users WHERE Login = @login;";
          DB.get().CommandParameters.Add(new SqlParameter("@login", username));

          int id = -1;

          int.TryParse(DB.get().GetOneCell(query).ToString(), out id);

          return id;
        }
      }
      public class DB
      {
    ...........
    private static DB instance;

        public static DB get()
        {
          if (instance == null)
            instance = new DB();

          return instance;
        }

        private SqlConnection connection;
        private SqlDataReader reader;

        public List CommandParameters;
        private DB()
        {
          this.connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["DBConnectionString"].ConnectionString);
          this.CommandParameters = new List();
        }

       public object GetOneCell(string query)
        {
          SqlCommand sc = new SqlCommand(query, this.connection);

          if (this.CommandParameters.Count != 0)
            sc.Parameters.AddRange(this.CommandParameters.ToArray());

          this.connection.Open();

          object res = sc.ExecuteScalar();
          this.CommandParameters.Clear();

         this.Close();
          return res;
        }
      }
    }

    * This source code was highlighted with Source Code Highlighter.

    Add a new unit test to the project and get a file with the following content:


    using System;
    using System.Text;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    namespace LinkCatalog.Tests
    {
      [TestClass]
      public class UserModel_Tests
      {
        [TestMethod]
        public void TestMethod1()
        {
        }
      }
    }

    * This source code was highlighted with Source Code Highlighter.

    The [TestClass] attribute means that this class contains test methods, and [TestMethod] means that such a method is a specific method.
    Add a link to the DAL assembly in the project and import the LinkCatalog.DAL namespace. The preparatory work is finished, it is time to write tests.
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    namespace LinkCatalog.Tests
    {
      using LinkCatalog.DAL;
      [TestClass]
      public class UserModel_Tests
      {
        [TestMethod]
        public void GetUserById_Test()
        {
          Assert.AreNotEqual(UserModel.GetUserIdByName("Admin"), -1);
        }
      }
    }

    * This source code was highlighted with Source Code Highlighter.

    We always have an administrator with such a login, and therefore his identifier cannot be -1.
    Run the test - and the error: An exception popped up:


    Test method LinkCatalog.Tests.UserModel_Tests.GetUserById_Test threw exception:
    System.NullReferenceException: Object reference not set to an instance of an object.
    LinkCatalog.DAL.DB..ctor() in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\DAL\Database.cs: line 35
    LinkCatalog.DAL.DB.get() in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\DAL\Database.cs: line 17
    LinkCatalog.DAL.UserModel.GetUserIdByName(String username) in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\DAL\Models\UserModel.cs: line 63
    LinkCatalog.Tests.UserModel_Tests.GetUserById_Test() in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\LinkCatalog.Tests\UserModel_Tests.cs: line 12

    * This source code was highlighted with Source Code Highlighter.

    As you can see, the error lies in the constructor of the DB class - it cannot find the configuration file and, as a result, the test fails not only unsuccessfully, but also with an error.

    The solution to the configuration file problem is quite simple:
    No, the test environment will not automatically connect web.config. Instead, each test project creates its own app.config configuration file, and all that is required is to add the necessary settings to it.



     
      

     
     
      
           CommandTimeout="30" />
           CommandTimeout="30" />
     


     
     
           providerName="System.Data.SqlClient" />
     

     


    * This source code was highlighted with Source Code Highlighter.

    Now the test is successful: Let's change the test a little


        [TestMethod]
        public void GetUserById_Test()
        {
          Assert.AreNotEqual(UserModel.GetUserIdByName("Admin"), -1);

          Assert.AreEqual(UserModel.GetUserIdByName("0-934723  ### 12sdf s"), -1);
        }

    * This source code was highlighted with Source Code Highlighter.

    There is no guarantee of such a login in the database (the login should be without spaces, this is followed by validators in the controller during registration), and this is the second and last test of the method. The test fails again - we look at the details:


    Test method LinkCatalog.Tests.UserModel_Tests.GetUserById_Test threw exception:
    System.NullReferenceException: Object reference not set to an instance of an object.

    На этот раз ошибка заключается в UserModel.GetUserIdByName, а именно вот здесь не хватает проверки на null:
          int.TryParse(DB.get().GetOneCell(query).ToString(), out id);
    Добавляем:
    var res = DB.get().GetOneCell(query);
          if (res != null)
            int.TryParse(res.ToString(), out id);

    * This source code was highlighted with Source Code Highlighter.


    Now everything is OK! This is how testing helps to identify some errors in the program, even if they arise from forgetfulness / laziness / not knowledge, but it is easier to fix them this way than on a live server. This topic, of course, does not cover all aspects of program testing and only opened this area. Behind the scenes was the possibility of conditional tests that depend on other tests, user interface tests, and other things. Test your programs and have few bugs, but many, many features!




    Also popular now: