对于熟悉Cmockery的读者,这里所示的桩函数和测试用例或许看起来更有感觉。

    stub_dll.c
    dll_node_t *dll_pop_head (dll_t *_p_dll)
    {
        return (dll_node_t *)mock ();
    }
    
    test_mpool.c
    void test_mpool_buffer_alloc ()
    {
        mpool_node_t mnode;
    
        // set up test environment
        mnode.addr_ = 0x5A5A5A5A;
        mnode.in_use_ = false;
    
        // do test
        will_return (dll_pop_head, &mnode.node_);
        assert_int_equal (mpool_buffer_alloc (handler), 0x5A5A5A5A);
        will_return (dll_pop_head, 0);
        assert_int_equal (mpool_buffer_alloc (handler), 0);
    }

  需要指出的是,通过打桩的方式,既可以完成状态检验(State Verification),也可以完成行为检验(Behavior Verification),这完全取决于桩函数的实现(本文的示例是状态检验)。关于状态检验与行为检验更为详细的内容,请参见Martin Fowler的《Mocks aren’t Stubs》。

  对于没有单元测试经验的读者来说,这里的示例会让你对单元测试有一定的了解。而对于有单元测试经验的读者来说,一定会想到采用打桩的方式所带来的实施困境。第一,桩函数对被替换函数的行为模拟越接近,单元测试的效果越好,但所花费的成本开销也越大。极端情况下,会发现桩代码与桩所替换的代码在规模上是相当的。在产品的按时交付压力之下,实施单元测试所造成的软件规模增大很难让团队做到真心拥抱单元测试。第二,当项目规模增大以后,维护单元测试的桩函数并不是一件简单的事情。项目规模的增大,易造成各个子团队维护重复的桩代码。即使整个项目有着很好的规划,将所有的桩都以库的形式进行集中维护,但单元测试代码的编译、桩代码与项目代码的同步维护仍需相当可观的工作量。要走出这两大困境,需要我们单元测试做一点小小的观念转变 — 放弃打桩。

  想一想,为什么不将桩与其所替代的项目代码整合在一起,从而省去打桩呢?此时,单元测试的实施需要用到我在《专业嵌入式软件开发》一书中所提出的错误注入的方法。大体上,错误注入的思想与前面图2中实现单元测试的方法几乎一样,但是将桩函数的代码与所替换的产品代码进行了合并。下列是引入错误注入概念之后dll_pop_head函数的实现。

    dll.c
    dll_node_t *dll_pop_head (dll_t *_p_dll)
    {
        dll_node_t *p_node = _p_dll->head_;
    
    #ifdef UNIT_TESTING
        {
            dll_node_t *p_node;
            error_t ecode = injected_error_get (
                INJECTION_POINT_DLL_POP_HEAD, &p_node);
            if (ecode != 0) {
                return p_node;
            }
        }
    #endif
    
        if (p_node != 0) {
            _p_dll->count_--;
            _p_dll->head_ = p_node->next_;
            if (0 == _p_dll->head_) {
                _p_dll->tail_ = 0;
            }
            else {
                p_node->next_->prev_ = 0;
            }
            p_node->next_ = 0;
            p_node->prev_ = 0;
        }
        
        return p_node;
    }