Lab: Unit testing with mocks

This article is based on unit-testing article and code labs from:

 



Part 1

Let us practice unit-testing, with a little bit of TDD thrown into the mix.

steering.h: This is just a header file and we will Mock out this file and therefore you do not need to write this file's implementation.

#pragma once

void steer_left(void);
void steer_right(void);

steer_processor.h: You will write the implementation of this file yourself at steer_processor.c

#pragma once

#include <stdint.h>

#include "steering.h"

/**
 * Assume that a threshold value is 50cm
 * Objective is to invoke steer function if a sensor value is less than the threshold
 *
 * Example: If left sensor is 49cm, and right is 70cm, then we should call steer_right()
 */
void steer_processor(uint32_t left_sensor_cm, uint32_t right_sensor_cm);

test_steer_processor.c You will write the test code, before you write the implementation of steer_processor() function.

#include "unity.h"

#include "steer_processor.h"

#include "Mocksteering.h"

void test_steer_processor__move_left(void) { }

void test_steer_processor__move_right(void) { }

void test_steer_processor__both_sensors_less_than_threshold(void) { }

// Hint: If you do not setup an Expect()
// then this test will only pass none of the steer functions is called
void test_steer_processor__both_sensors_more_than_threshold(void) {

}

// Do not modify this test case
// Modify your implementation of steer_processor() to make it pass
// This tests corner case of both sensors below the threshold
void test_steer_processor(void) {
  steer_right_Expect();
  steer_processor(10, 20);

  steer_left_Expect();
  steer_processor(20, 10);
}

Do the following:

  • Put the steering.h in your source code
  • Put the steer_processor.h in your source code
  • Put the test_steer_processor.c in your test code folder
  • Write the implementation of test_steer_processor.c and run the tests to confirm failing tests
  • Write the implementation of steer_processor.c

 



Part 2

Write the unit-tests first, and then the implementation for the following header file:

#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

/* In this part, the queue memory is statically defined
 * and fixed at compile time for 100 uint8s
 */
typedef struct {
  uint8_t queue_memory[100];

  // TODO: Add more members as needed
} queue_s;

// This should initialize all members of queue_s
void queue__init(queue_s *queue);

/// @returns false if the queue is full
bool queue__push(queue_s *queue, uint8_t push_value);

/// @returns false if the queue was empty
bool queue__pop(queue_s *queue, uint8_t *pop_value);

size_t queue__get_item_count(const queue_s *queue);

Students often time create non optimal and incorrect implementation of a queue. Remember that a queue means FIFO data structure, which means oldest item pushed should be the first one out of the pop operation. Here are some unit tests that you are required to add to your test. This test will ensure that your implementation is correct.

void test_comprehensive(void) {
  const size_t max_queue_size = 100; // Change if needed
  
  for (size_t item = 0; item < max_queue_size; item++) {
    TEST_ASSERT_TRUE(queue__push(&queue, item));
    TEST_ASSERT_EQUAL(item + 1, queue__get_item_count(&queue));
  }
  
  // Should not be able to push anymore
  TEST_ASSERT_FALSE(queue__push(&queue, 123));
  TEST_ASSERT_EQUAL(max_queue_size, queue__get_item_count(&queue));
  
  // Pull and verify the FIFO order
  for (size_t item = 0; item < max_queue_size; item++) {
    size_t popped_value = 0;
    TEST_ASSERT_TRUE(queue__pop(&queue, &popped_value));
    TEST_ASSERT_EQUAL(item, popped_value);
  }
  
  TEST_ASSERT_EQUAL(0, queue__get_item_count(&queue));
  TEST_ASSERT_FALSE(queue__pop(&queue, &popped_value));
}


Part 3

Write the unit-tests first, and then the implementation for the following header file. This is a slight variation of Part 2 and it provides you with the static memory based programming pattern popular in Embedded Systems where we deliberately avoid allocating memory on the heap.

#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

/* In this part, the queue memory is statically defined
 * by the user and provided to you upon queue__init()
 */
typedef struct {
  uint8_t *static_memory_for_queue;
  size_t static_memory_size_in_bytes;

  // TODO: Add more members as needed
} queue_s;

/* Initialize the queue with user provided static memory
 * @param static_memory_for_queue This memory pointer should not go out of scope
 *
 * @code
 *   static uint8_t memory[128];
 *   queue_s queue;
 *   queue__init(&queue, memory, sizeof(memory));
 * @endcode
 */
void queue__init(queue_s *queue, void *static_memory_for_queue, size_t static_memory_size_in_bytes);

/// @returns false if the queue is full
bool queue__push(queue_s *queue, uint8_t push_value);

/// @returns false if the queue was empty
/// Write the popped value to the user provided pointer pop_value_ptr
bool queue__pop(queue_s *queue, uint8_t *pop_value_ptr);

size_t queue__get_item_count(const queue_s *queue);

 



Part 4

In this part, the objectives are:

  • Practice StubWithCallback or ReturnThruPtr
  • Ignore particular arguments

message.h: This is just an interface, and we will Mock this out.

#pragma once

#include <stdbool.h>

typedef struct {
  char data[8];
} message_s;

bool message__read(message_s *message_to_read);

message_processor.c: This code module processes messages arriving from message__read() function call. There is a lot of nested logic that is testing if the third message contains $ or # at the first byte. To get to this level of the code, it is difficult because you would have to setup your test code to return two dummy messages, and a third message with particular bytes.

To improve test-ability, you should refactor the } else { logic into a separate static function that you can hit with your unit-tests directly.

#include <stdbool.h>
#include <stddef.h>
#include <string.h>

#include "message_processor.h"

/**
 * This processes messages by calling message__read() until:
 *   - There are no messages to process -- which happens when message__read() returns false
 *   - At most 3 messages have been read
 */
bool message_processor(void) {
  bool symbol_found = false;
  message_s message;
  memset(&message, 0, sizeof(message));

  const static size_t max_messages_to_process = 3;
  for (size_t message_count = 0; message_count < max_messages_to_process; message_count++) {
    if (!message__read(&message)) {
      break;
    } else {
      if (message.data[0] == '$') {
        symbol_found = true;
      } else {
        // Symbol not found
      }
    }
  }

  return symbol_found;
}

test_message_processor.c: Add more unit-tests to this file as needed.

#include "unity.h"

#include "Mockmessage.h"

#include "message_processor.h"

// This only tests if we process at most 3 messages
void test_process_3_messages(void) {
  message__read_ExpectAndReturn(NULL, true);
  message__read_IgnoreArg_message_to_read();

  message__read_ExpectAndReturn(NULL, true);
  message__read_IgnoreArg_message_to_read();

  message__read_ExpectAndReturn(NULL, true);
  message__read_IgnoreArg_message_to_read();

  // Since we did not return a message that starts with '$' this should return false
  TEST_ASSERT_FALSE(message_processor());
}

void test_process_message_with_dollar_sign(void) {
}

void test_process_messages_without_any_dollar_sign(void) {
}

// Add more tests if necessary

 



Requirements
  • Test thoroughly
    • Do not hack internals of a module.
    • This means that only operate using the APIs, and do not modify the data structure
    • As an example, to test pop(), push elements using the API rather than hacking struct.write_index++
  • Create a thorough test like this one at the end of your basic tests:
    • Push to the capacity of the queue
    • Then pop all elements
    • Finally push value of 0x1A and pop value of 0x1A
  • Do not "shift" any elements in your pop() operation
    • Keep track of read and write indexes separately
    • It would be horrible pop operation that has to shift thousands of elements over by 1
  • Pop test should explicitly test to make sure the popped value is what was pushed
    • This means that the pop() API depends on the push() API to work
  • Each test should construct a new queue, you should not rely on previous test to run before the current test
    • setUp() method may be used to re-create and re-initialize a new queue each time
Back to top