Three Steps to less duplication

Like I said in my previous blog, doing all this embedded C makes me miss constructors. I’ve got a three step plan to make the lack of constructors less painful. In the previous article, we discussed the problem of duplicate setup data, and all the duplicate effort to go along with it. In this article I’ll tell you what we’re doing about it.

One effort is non-technical, and probably the most significant. That’s getting the organization to communicate! The teams have to follow recognizable conventions and communicate the sharable work that they do. The first step is awareness of the problem, and the second is proposing a solutions. Then it’s all just implementation.

We got a lot of people aware of the problem, and proposed a convention to follow. If implemented, it goes a long way in improving the situation. We’re still programming in C, so we won’t get rid of the problems of duplicate initialization completely, but we can make it a lot better.

We’ve all heard of 12 step plans, but I think we can get by with three. Well, maybe I am ignoring a couple steps like: admitting the problem, and the decision to do something about it. So maybe it’s a 5 step plan.

Step 1) What’s the problem? Setup code is hard to write and hard to share. Effort is duplicated, code is duplicated. This example shows some custom initialization code. Imagine this and similar initialization code repeated in multiple test cases, and independently developed by different teams.

  int system_init_for_mytest()
  {
      int result = 0;
      int important_data_type = 12;
      int start_time = 14;
      message_t msg;
      ip_address_t controller_addr;
 
      memset(&msg, 0, sizeof(msg));
      init_ip_address(controller_addr, 127, 1, 0 ,0);
      msg.id = 1;
      msg.type = CONFIG;
      msg.types.confuguration.age = 44
      msg.types.confuguration.dictionary = DEFAULT;
      msg.types.confuguration.modifier = NONE;
      msg.types.confuguration.name = "A";
 
      result = system_init(
                          &msg,
                          important_data_type,
                          start_time,
                          &controller_addr); 
 
      return result;
  }

Step 2) We should we do something. Why? We’re going to have this code a long time. Much longer that the current sprint or current release. So we need to invest in keeping this code clean, and maintainable, every day.

OK, we’ll do something. There are three specific technical things to do:

Step 3) Copy the setup data from the custom initialization function and put it into a structure. The struct contains the parameters needed to call the production code.

typedef struct
{
    int result;
    int important_data_type;
    int start_time;
    message_t msg;
    ip_address_t controller_addr;
} system_parameters_t;

Step 4) Separate parameter initialization from system initialization. This lets us use a default initialization but gives us a chance to change from the default before actually initializing the production code.

void system_parameters_default(system_parameters_t* p)
{
    p->result = 0;
    p->important_data_type = 12;
    p->start_time = 14;
    memset(&p->msg, 0, sizeof(p->msg));
    init_ip_address(p->controller_addr, 127, 1, 0 ,0);
    p->msg.id = 1;
    p->msg.type = CONFIG;
    p->msg.types.confuguration.age = 45;
    p->msg.types.confuguration.dictionary = DEFAULT;
    p->msg.types.confuguration.modifier = NONE;
    p->msg.types.confuguration.name = "A";
}

system_parameters_default provides a convenient way to set the default
version of the system parameters.

Step 5) Do the production code initialization.

int system_init_for_test(system_parameters_t* p)
{
   result = system_init(
                        &p->msg,
                        p->important_data_type,
                        p->start_time,
                        &p->controller_addr); 
 
    return result;
}

With these pieces in place we can reuse the default configuration and easily modify individual parameters. Here’s an example usage.

TEST_GROUP(MyTestGroup)
{
    system_parameters_t system_parameters;
 
    void setup()
    {
        system_parameters_default(&system_parameters);
    }
    void teardown()	
    {
    }
 
    void initializeSystem()
    {
        LONGS_EQUAL(OK,
            system_init_for_test(&system_parameters));
    }
};

In the nominal case no additional initialization is needed.

TEST( MyTestGroup, NominalCase)
{ 
    initializeSystem();
    CHECK(/* normal case */);
}

In each special case some small part of the initialization is changed before initializing the production code.

TEST( MyTestGroup, ConfigurationB)
{ 
    system_parameters.msg.types.confuguration.name 
                                = "B";
    initializeSystem();
    CHECK(/* when for the name is "B" */);
}

This is a simple example. In reality we had multiple stages of initialization to create the data structure environment needed to run our test cases. So initialization can be quite complex. An interesting side effect of this form of setup is that the differences between test cases becomes very obvious. If you are tasked with a modification to this area, you can review the default initialization and leverage your knowledge of that initialization over all the tests that share it.

We’re not done yet. Just building the initialization helper is not enough. Like the doomsday machine in Dr. Strangelove, you have to tell the world!. So there are a couple more steps.

Step 6) Inform

But many won’t hear the first time.

Step 7) Remind (Some might not listen, then nag)

So goes the three-step plan.

Leave a Reply

Your email address will not be published. Required fields are marked *

Be gone spammers * Time limit is exhausted. Please reload the CAPTCHA.