[嵌入式TDD实战]TDD开发AT26DF驱动模块(三):第一个测试用例

前言

上一章中,我们把模块的通信监控框架搭起来了。万事俱备,直接开始开发吧。

第一个接口

函数签名

我们来看看AT26DF的第一条命令:

嗯,Read Array,那第一个接口的名字就叫AT26DF_readArray了。

  • 这个方法在CS断言后,先要求一个操作符,0B比03的好处是没有频率限制,那感情好。先写0B那个。
  • 然后需要一个24bit的地址,那接口的参数中加一个地址参数。
  • 因为选择了0B,还需要多传一个随便什么字符
  • 然后就可以从这个地址起想读几个字符读几个了,那说明还要一个指定读多少字节的参数,以及一个告知把返回结果写到哪的参数
  • 最后通过CS取消断言来结束

那这个接口的签名就基本确定了:

……
typedef uint32_t AT26DF_ADDR;
……
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len);

虽然用了32位的int表示了实际只需要24位的地址,但没事,只取低24位就好,主要比byte[3]用起来方便的多。
重新定义下类型后这个参数做什么用的也一目了然。完美!

开写测试用例

那我们接下来为他写测试用例,先把监听搭好,随便给几个参数(这里已经不需要那个NULL测试了,删掉吧):
AT26DFTest.c

#include "unity_fixture.h"
#include "AT26DF.h"
#include "MockSPI.h"

TEST_GROUP(AT26DF);
TEST_SETUP(AT26DF){
}

TEST_TEAR_DOWN(AT26DF){
}

TEST(AT26DF, ReadArray){
  MockSPI_Init();
  AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);

  AT26DF_readArray(0x345254, actualData, 5);

  MockSPI_Verify();
  MockSPI_Destroy();
}

生成试试。

咦,报错了,哦,光写了声明,没写实现。我们打开AT26DF.c文件,直接写个空函数先,TDD的基本原则就是没有写测试前不要写代码。

AT26DF.c

……
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
}

再次生成。


编译成功了,但这数量咋不对呢。哦,新测试忘记安装进Runner了。

AT26DFTestRunner.c

#include "unity_fixture.h"

TEST_GROUP_RUNNER(AT26DF)
{
  RUN_TEST_CASE(AT26DF, ReadArray)
}

再生成。

数量对了。

然后往测试用例里头填内容。

首先应该要先断言:

  ……
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
  SPI_SelectCS_Expect();

  AT26DF_readArray(0x345254, actualData, 5);
  ……

生成:

果然出问题了,赶快回去在实现中加上这一句。

void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
}

生成,通过!

好嘞,然后,首先应该传一个操作符0x0B

……
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(0x0B);

  AT26DF_readArray(0x345254, actualData, 5);
……

再测试,又失败了

嗯,要回去加上写操作符了,但是为了让你明白mock真能测出你没按希望的参数调用,我们故意失误,打错了输出的错误码:

void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
  _spiWrite(0x03);
}

这时来看看测试输出:

输出中准确地说明了测试用例失败的原因。

好啦,去改成正确的吧。

这时,我又觉得0x0B很不直观,这是个magic number,可读性差。所以我又改了改,给了它个名字,让它别那么magic了。

AT26DF.h

#ifndef _AT26DF_H
#define _AT26DF_H
#include <stdint.h>

#define AT26DF_OPC_READARRAY   0x0B

typedef uint32_t AT26DF_ADDR;
……

AT26DF.c

void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
  _spiWrite(AT26DF_OPC_READARRAY);
}

AT26DFTest.c

……
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READARRAY);

  AT26DF_readArray(0x345254, actualData, 5);
……

这样,以后的操作码我也都会用宏定义的方式给它个AT26DF_OPC_XXXX形式的名字。在测试用例和接口实现中都用名字的坏处是如果定义错了,你难以发现。但是没事,在硬件测试中会发现的。为了可读性还是值得的。

接下来,地址,传过去的是0x345254,那也应该收到对应的:

……
  SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
  // address byte
  SPI_writeByte_Expect(0x34);
  SPI_writeByte_Expect(0x52);
  SPI_writeByte_Expect(0x54);

  AT26DF_readArray(0x345254, actualData, 5);
……

测试,失败,然后修改实现:

void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
  _spiWrite(AT26DF_OPC_READARRAY);
  _spiWrite((addr >> 16) & 0xFF);
  _spiWrite((addr >> 8) & 0xFF);
  _spiWrite(addr & 0xFF);
}

测试通过。

然后应该写一个无所谓什么字节,CMock中是这么写的

  SPI_writeByte_Expect(0x54);
  // one don't care byte
  SPI_writeByte_Expect(0xab);
  SPI_writeByte_IgnoreArg_b();

  AT26DF_readArray(0x345254, actualData, 5);

SPI_writeByte_IgnoreArg_b使得上一个SPI_writeByte_Expect忽视参数b。
这里又有magic number的问题了,为了让后人看到这个0xab不感到困惑,咱们略作修改。

……
TEST_GROUP(AT26DF);
static const uint8_t anyByte = 0xEE;
TEST_SETUP(AT26DF){
}
……
  SPI_writeByte_Expect(0x54);
  // one don't care byte
  SPI_writeByte_Expect(anyByte);
  SPI_writeByte_IgnoreArg_b();

  AT26DF_readArray(0x345254, actualData, 5);

然后又是,测试,失败,然后改造实现:

……
#define DONTCAREBYTE   0x37
……
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
  _spiWrite(AT26DF_OPC_READARRAY);
  _spiWrite((addr >> 16) & 0xFF);
  _spiWrite((addr >> 8) & 0xFF);
  _spiWrite(addr & 0xFF);

  _spiWrite(DONTCAREBYTE);
}

好了,这个没用的字符发出去后就该开始读取数据了。
读取多少个是由len参数决定的,而返回的应该就是SPI_Read给他的,换句话说就是咱们的接口应该调用len次SPI_read然后把结果写到buf指向的数组中。
所以测试用例变成了这样:

TEST(AT26DF, ReadArray){
  int i;
  const uint8_t expectData[] = {0x12, 0x34, 0x56, 0x78, 0x9a};
  uint8_t actualData[sizeof(expectData)];
  const uint8_t len = sizeof(expectData);
  MockSPI_Init();
  AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
  // address byte
  SPI_writeByte_Expect(0x34);
  SPI_writeByte_Expect(0x52);
  SPI_writeByte_Expect(0x54);
  // one don't care byte
  SPI_writeByte_Expect(anyByte);
  SPI_writeByte_IgnoreArg_b();
  for(i = 0; i < len; i++){
    SPI_readByte_ExpectAndReturn(expectData[i]);
  }

  AT26DF_readArray(0x345254,actualData,len);

  TEST_ASSERT_EQUAL_HEX8_ARRAY(expectData,actualData,len);
  MockSPI_Verify();
  MockSPI_Destroy();
}

我们要验证了接口调用了N次的read,同时真的把结果返回来了。

测试,失败,然后改造实现:


void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
  _spiWrite(AT26DF_OPC_READARRAY);
  _spiWrite((addr >> 16) & 0xFF);
  _spiWrite((addr >> 8) & 0xFF);
  _spiWrite(addr & 0xFF);

  _spiWrite(DONTCAREBYTE);
  _spiBurstRead(buf,len);
}

测试通过,OK。最后别漏了个取消断言:

……
  for(i = 0; i < sizeof(expectData); i++){
    SPI_readByte_ExpectAndReturn(expectData[i]);
  }
  SPI_DeselectCS_Expect();

  AT26DF_readArray(0x345254,actualData,sizeof(expectData));
……

然后接口实现就成了这样:

void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
  _spiWrite(AT26DF_OPC_READARRAY);
  _spiWrite((addr >> 16) & 0xFF);
  _spiWrite((addr >> 8) & 0xFF);
  _spiWrite(addr & 0xFF);
  _spiWrite(DONTCAREBYTE);
  _spiBurstRead(buf,len);
  _csDesel();
}

这样,我们的第一个测试用例其实就写完了。

我们回头再看看写好的测试用例,

  MockSPI_Init();
  AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);

  MockSPI_Verify();
  MockSPI_Destroy();

其实是这个模块的所有测试通用的Setup和teardown部分,所以,我们再进行修改,把这两部分分别移到:TEST_SETUP和TEST_TEAR_DOWN里,这样,在所有这个测试组的测试用例开始前都会初始化MockSPI并注册,结束时都会验证MockSPI并回收资源。于是,到目前为止,我们的几个文件长这样:

本章快照

AT26DFTest.c

#include "unity_fixture.h"
#include "AT26DF.h"
#include "MockSPI.h"

TEST_GROUP(AT26DF);
static const uint8_t anyByte = 0xEE;
TEST_SETUP(AT26DF){
  MockSPI_Init();
  AT26DF_regFuncSPI(SPI_readByte,SPI_writeByte);
  AT26DF_regFuncCS(SPI_SelectCS,SPI_DeselectCS);
}

TEST_TEAR_DOWN(AT26DF){
  MockSPI_Verify();
  MockSPI_Destroy();
}

TEST(AT26DF, ReadArray){
  int i;
  const uint8_t expectData[] = {0x12, 0x34, 0x56, 0x78, 0x9a};
  uint8_t actualData[sizeof(expectData)];
  const uint8_t len = sizeof(expectData);
  SPI_SelectCS_Expect();
  SPI_writeByte_Expect(AT26DF_OPC_READARRAY);
  // address byte
  SPI_writeByte_Expect(0x34);
  SPI_writeByte_Expect(0x52);
  SPI_writeByte_Expect(0x54);
  // one don't care byte
  SPI_writeByte_Expect(anyByte);
  SPI_writeByte_IgnoreArg_b();
  for(i = 0; i < len; i++){
    SPI_readByte_ExpectAndReturn(expectData[i]);
  }
  SPI_DeselectCS_Expect();

  AT26DF_readArray(0x345254,actualData,len);

  TEST_ASSERT_EQUAL_HEX8_ARRAY(expectData,actualData,len);
}

AT26DFTestRunner.c

#include "unity_fixture.h"

TEST_GROUP_RUNNER(AT26DF)
{
  RUN_TEST_CASE(AT26DF, ReadArray);
}

AT26DF.h

#ifndef _AT26DF_H
#define _AT26DF_H
#include <stdint.h>

#define AT26DF_OPC_READARRAY   0x0B

typedef uint32_t AT26DF_ADDR;

// description: Register SPI call back functions
// parameter  : spi_rb   callback function to read byte using SPI
//              spi_wb   callback function to write byte using SPI
// return     : 
// note       : you should register SPI functions through this interface before use the driver.
void AT26DF_regFuncSPI(uint8_t (*spi_rb)(void), void (*spi_wb)(uint8_t wb));
// description: Register SPI call back functions
// parameter  : spi_rb   callback function to burst read byte using SPI
//                       NULL   if want to use the default function
//              spi_wb   callback function to burst write byte using SPI
//                       NULL   if want to use the default function
// return     : 
// note       : if you don't register any functions, the default functions will call the functions
//              registered by the AT26DF_regFuncSPI.
void AT26DF_regFuncSPIBurst(void (*spi_rb)(uint8_t* pBuf, uint16_t len), 
                             void (*spi_wb)(const uint8_t* pBuf, uint16_t len));
// description: Registers call back function for AT26DF select & deselect.
// parameter  : cs_sel     callback function for WIZCHIP select
//              cs_desel   callback fucntion for WIZCHIP deselect
// return     : 
// note       : you should register CS functions through this interface before use the driver.
void AT26DF_regFuncCS(void(*cs_sel)(void), void(*cs_desel)(void));
// description: Sequentially read a continuous stream of data begun from the address specified.
// parameter  : addr   the starting address
//              buf    buffer to store the result.
//              len    the length of data you want to read.
// return     : 
// note       : this function can be used at any SCK frequency up to the maximum specified by fSCK.
void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len);

#endif

AT26DF.c

#include "common.h"
#include "AT26DF.h"
#define DONTCAREBYTE   0x37

static uint8_t _spi_rb_default(void);
static void _spi_wb_default(uint8_t wb);
static void _spi_burst_rb_default(uint8_t* pBuf, uint16_t len);
static void _spi_burst_wb_default(const uint8_t* pBuf, uint16_t len);
static void _nullFunc(void);

static uint8_t (*_spiRead)(void) = _spi_rb_default;
static void (*_spiWrite)(uint8_t wb) = _spi_wb_default;
static void (*_spiBurstRead)(uint8_t* pBuf, uint16_t len) = _spi_burst_rb_default;
static void (*_spiBurstWrite)(const uint8_t* pBuf, uint16_t len) = _spi_burst_wb_default;
static void(*_csSel)(void) = _nullFunc;
static void(*_csDesel)(void) = _nullFunc;

static uint8_t _spi_rb_default(void){return 0;}
static void _spi_wb_default(uint8_t wb){(void)wb;}
static void _spi_burst_rb_default(uint8_t* pBuf, uint16_t len){
  while(len > 0){
    --len;
    *pBuf++ = _spiRead();
  }
}
static void _spi_burst_wb_default(const uint8_t* pBuf, uint16_t len){
  while(len > 0){
    --len;
    _spiWrite(*pBuf++);
  }
}

static void _nullFunc(void){}


void AT26DF_regFuncSPI(uint8_t (*spi_rb)(void), void (*spi_wb)(uint8_t wb)){
  _spiRead = spi_rb;
  _spiWrite = spi_wb;
}

void AT26DF_regFuncSPIBurst(void (*spi_rb)(uint8_t* pBuf, uint16_t len), void (*spi_wb)(const uint8_t* pBuf, uint16_t len)){
  if(spi_rb)
    _spiBurstRead = spi_rb;
  else
    _spiBurstRead = _spi_burst_rb_default;
  if(spi_wb)
    _spiBurstWrite = spi_wb;
  else
    _spiBurstWrite = _spi_burst_wb_default;
}

void AT26DF_regFuncCS(void(*cs_sel)(void), void(*cs_desel)(void)){
  _csSel = cs_sel;
  _csDesel = cs_desel;
}


void AT26DF_readArray(AT26DF_ADDR addr, uint8_t *buf, uint16_t len){
  _csSel();
  _spiWrite(AT26DF_OPC_READARRAY);
  _spiWrite((addr >> 16) & 0xFF);
  _spiWrite((addr >> 8) & 0xFF);
  _spiWrite(addr & 0xFF);
  _spiWrite(DONTCAREBYTE);
  _spiBurstRead(buf,len);
  _csDesel();
}

结语

这一章中,我较为详尽的展示了用TDD的方式开发单个接口的各个步骤。不断的重复TDD微循环:

  • 添加一个小测试
  • 运行所有测试并故意看到新的那个失败,甚至编译不了。
  • 做很小的修改使得测试通过。
  • 运行所有测试并看到新的一个通过。
  • 重构,移除冗余并提升代码可读性。

测试用例也遵从 四步测试模式:

  • Setup:建立测试的预环境(precondition)
  • Exercise:做一些事情
  • Verify:检查输出是否如期望的
  • Cleanup:在测试后把系统还原为最初状态


感兴趣的朋友可以读《测试驱动的嵌入式c语言开发》更深入的学习,这是我的笔记

后面的章节中,我不再事无巨细的按照一个个测试的小步骤来讲,而可能会更偏向于重构的思想来讲。但是读者要清楚,实际上还是遵从着这章中的模式一点点递增地开发出程序的。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值