Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practices TDD on complex objects

I am trying to become more familiar with test driven development. So far I have seen some easy examples, but I have still problems approaching complex logic like for example this method in my DAL:

public static void UpdateUser(User user)
        {
            SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["WebSolutionConnectionString"]);
            SqlCommand cmd = new SqlCommand("WS_UpdateUser", conn);

            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Add("@UserID", SqlDbType.Int, 4);
            cmd.Parameters.Add("@Alias", SqlDbType.NVarChar, 100);
            cmd.Parameters.Add("@Email", SqlDbType.NVarChar, 100);
            cmd.Parameters.Add("@Password", SqlDbType.NVarChar, 50);
            cmd.Parameters.Add("@Avatar", SqlDbType.NVarChar, 50);
            cmd.Parameters[0].Value = user.UserID;
            cmd.Parameters[1].Value = user.Alias;
            cmd.Parameters[2].Value = user.Email;
            cmd.Parameters[3].Value = user.Password;
            if (user.Avatar == string.Empty)
                cmd.Parameters[4].Value = System.DBNull.Value;
            else
                cmd.Parameters[4].Value = user.Avatar;

            conn.Open();
            cmd.ExecuteNonQuery();
            conn.Close();
        }

What would be good TDD practices for this method?

like image 748
MUG4N Avatar asked Feb 20 '23 14:02

MUG4N


1 Answers

Given that the code is already written, let's talk about what makes it hard to test instead. The main issue here is that this method is purely a side-effect: it returns nothing (void), and its effect is not observable in your code, in object-land - the observable side effect should be that somewhere in a database far away, a record has now been updated.

If you think of your unit tests in terms of "Given these conditions, When I do this, Then I should observe this", you can see that the code you have is problematic for unit testing, because the pre-conditions (given a connection to a DB that is valid) and post-conditions (a record was updated) are not directly accessible by the unit test, and depend on where that code is running (2 people running the code "as is" on 2 machines have no reason to expect the same results).

This is why technically, tests that are not purely in-memory are not considered unit tests, and are somewhat out of the realm of "classic TDD".

In your situation, here are 2 thoughts:

1) Integration test. If you want to verify how your code works with the database, you are in the realm of integration testing and not unit testing. TDD inspired techniques like BDD can help. Instead of testing a "unit of code" (usually a method), focus on a whole user or system scenario, exercised at a higher level. In this case for instance you could take it at a much higher level, and assuming that somewhere on top of your DAL you have methods called CreateUser, UpdateUser, ReadUser, the scenario you may want to test is something like "Given I created a User, When I update the User Name, Then when I Read the user the name should be updated" - you would then be exercising the scenario against a complete setup, involving data as well as DAL and possibly UI.

I found the following MSDN article on BDD + TDD interesting from that regard - it illustrates well how the 2 can mesh together.

2) If you want to make your method testable, you have to expose some state. The main part of the method revolves around Building a command. You could outline the method that way:

* grab a connection
* create the parameters and types of the command
* fill in the parameters of the command from the object
* execute the command and clean up

You can actually test most of these steps: the observable state is then the Command itself. You could have something along these lines:

public class UpdateUserCommandBuilder
{
   IConnectionConfiguration config;

   public void BuildAndExecute(User user)
   {
      var command = BuildCommand(user);
      ExecuteCommand(command);
   }

   public SqlCommand BuildCommand(User user)
   {
      var connection = config.GetConnection(); // so that you can mock it
      var command = new SqlCommand(...)

      command = CreateArguments(command); // you can verify that method now
      command = FillArguments(command, user); // and this one too

      return command;
   }
}

I won't go all the way here, but I assume the outline conveys the idea. Going that route would help make the steps of the builder verifiable: you could assert whether a correct command was created. This has some value, but would still tell you nothing about whether the execution of the command succeeded, so it's worth thinking whether this is a worthwhile use of your testing budget! Arguably, a higher-level integration test which exercises the whole DAL may be more economical.

Hope this helps!

like image 186
Mathias Avatar answered Mar 08 '23 03:03

Mathias