在前面的章节中,我们通过INoteRepository接口定义了笔记对象相关的操作。同时,为了快速实现我们的应用程序的功能,并且尽快能够看到交互效果,我们撇开数据库,编写了一个用于测试的临时的数据仓库实现,也就是TestNoteRepository类。
TestNoteRepository类实现了INoteRepository接口中定义的操作,并且完全能够实现App与用户的交互。但是TestNoteRepository类并没有实现数据的持久化存储。当应用程序退出后,用户数据就丢失了。
在这一节,我们基于数据库创建一个可持久化存储的INoteRepository实现,将其命名为NoteRepository。
下图展示的关系表明TestNoteRepository和NoteRepository分别是INoteRepository接口的一个实现:

0. 修改INoteRepository接口
目前INoteRepository接口声明了3项操作:
public interface INoteRepository {
ArrayList<Note> getAllNotes();
/**
* 保存笔记对象
* @param note 被保存的笔记对象
* @return 保存成功则返回true,失败返回false
*/
boolean saveNote(Note note);
/**
* 从存储中获取指定的笔记对象
* @param noteId
* @return
*/
Note getNote(long noteId);
}
在开发过程中,我们逐步发现其中的saveNote()操作在设计上不是很便利——如果设置其返回值为保存后的Note对象将更便于使用。因此,修改saveNote()方法定义如下:
public interface INoteRepository {
...
/**
* 保存笔记对象
* @param note 被保存的笔记对象
* @return 保存成功则返回被保存的Note对象,并带有id,失败返回null
*/
Note saveNote(Note note);
...
}
当然,这个修改必然导致接口的实现类TestNoteRepository的修改。不过工作量不算大。打开TestNoteRepository.java文件,找到saveNote()方法(此时应当被红色波浪线标记为出错),修改如下:
@Override
public Note saveNote(Note note) {
if (note != null) {
notes.add(0, note);
}
return note;
}
1. 定义新的数据仓库实现类
选中repository包,点击右键并在弹出菜单中选择“New -> Java Class”,在弹出的对话框中填写类名(name项)为NoteRepository,并使其实现INoteRepository接口:

注意:无论是编写代码还是填写对话框,都要尽可能借助Android Studio提供的自动填充功能,以防纯手工键入浪费体力并且极易产生错误
打开新生成的NoteRepository.java文件。和我们预料的一样,代码中出现红色波浪线提示出错,因为缺少INoteRepository接口规定的3个操作。
将光标定位到红色波浪线,按快捷键“Alt+Enter”,在弹出的菜单中选择“Implement methods”:

可以预判一下,我们新的数据仓库类的几个操作的实现将依赖于上一节中创建的数据库访问类NoteDAO。为了使NoteDAO类正常工作,需要为它提供Context对象,因此,为我们在NoteRepository类最开始的地方为它增加一个Context类型的属性:
public class NoteRepository implements INoteRepository {
private Context context;
...
}
围绕这个属性,添加构造方法,同时将NoteRepository类改写成单例模式:
private Context context;
// 私有构造方法
private NoteRepository(Context context) {
this.context = context;
}
private static NoteRepository sInstance;
//获取单例对象
public static NoteRepository getInstance(Context context) {
if (sInstance == null) {
sInstance = new NoteRepository(context);
}
return sInstance;
}
单例的实现与前面写过的单例基本上大同小异,不同之处是要向getInstance()方法提供Context类型的参数。
2. 为NoteDAO类添加按标题删除记录的方法
虽然目前我们的App中还没有考虑对数据进行删除,但是为了方便测试,我们为专门负责数据库访问的NoteDAO类增加一个按标题删除记录的方法。打开NoteDAO.java文件,为NoteDAO类添加deleteNoteByTitle()方法:
public void deleteNoteByTitle(String title) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
String selection = COL_TITLE + "='" + title + "'";
db.delete(TABLE_NOTE, selection, null);
}
3. 直接为NoteRepository类编写单元测试
目前我们还没有真正的着手编写NoteRepository类中三个数据操作方法。我们将编码和测试的顺序调换一下,先去为这三个方法编写单元测试用例,然后回过头来编写实现代码,直到测试用例运行通过。
测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。——《百度百科》
打开androidTest目录,在应用程序包名下创建新包repository,并创建NoteRepositoryTest类:

将该类标注为用于单元测试:
@RunWith(AndroidJUnit4.class)
public class NoteRepositoryTest {
}
在类中添加Context类型的属性,并进行初始化:
private Context context = InstrumentationRegistry.getTargetContext();
同时,创建被测试的NoteRepository类的对象并初始化:
private INoteRepository noteRepo = NoteRepository.getInstance(context);
-
为NoteRepository.saveNote()方法编写测试用例
为NoteRepositoryTest类添加名为testSaveNote()的方法,并用@Test将其标注为测试用例:
@Test
public void testSaveNote() {
}
测试的逻辑是:
- 新建一个笔记对象。
- 调用NoteRepository.saveNote()方法将其存入数据库并获得返回的保存后的笔记对象result。
- 如果保存成功,result对象应当不为空。即,测试通过标准即判断result是否为null。
编写测试代码如下:
@Test
public void testSaveNote() {
Note note = new Note(0, "testSaveNote", "Test 1", System.currentTimeMillis());
Note result = noteRepo.saveNote(note);
assertNotNull(result);
// 清除测试数据
NoteDAO.getInstance(context).deleteNoteByTitle("testSaveNote");
}
运行这个测试用例,不出意外的话测试必定失败,因为我们并没有真正的实现saveNote()方法的逻辑。现在我们就去NoteRepository类中修改saveNote()使其满足测试用例的要求。对于我们当前的需求,这个修改很简单,直接调用NoteDAO中提供的对应的操作即可:
@Override
public Note saveNote(Note note) {
return NoteDAO.getInstance(context).insertNote(note);
}
由于事先已经对NoteDAO类进行了单元测试,所以我们可以信任这个模块提供的功能。
再次运行单元测试,正常情况下测试通过,表明我们正确的按照测试用例的要求实现了saveNote()的功能。
-
为NoteRepository.getAllNotes()方法编写测试用例
同样,为NoteRepositoryTest类添加名为testGetAllNotes()的方法,并用@Test将其标注为测试用例:
@Test
public void testGetAllNotes() {
}
测试逻辑如下:
- 调用NoteRepository.saveNote()向数据库中存入3条笔记(该方法已经在前一用例中经过测试,因此可以信任)
- 调用NoteRepository.getAllNotes()方法获取全部笔记记录的列表
- 判断返回的列表是否为null,如果为null则测试失败(这就要求即使数据库中没有任何数据,也要返回一个长度为0的列表)
- 判断列表长度是否不小于3(可能有之前插入的其它数据),是则测试通过,否则测试失败
编写代码如下:
@Test
public void testGetAllNotes() {
Note note = new Note(0, "testGetAllNotes", "Test 1", System.currentTimeMillis());
noteRepo.saveNote(note);
noteRepo.saveNote(note);
noteRepo.saveNote(note);
ArrayList<Note> notes = noteRepo.getAllNotes();
assertNotNull(notes);
assertTrue(notes.size() >= 3);
// 清除测试数据
NoteDAO.getInstance(context).deleteNoteByTitle("testGetAllNotes");
}
运行这个测试,继续失败。那么我们修改NoteRepository.getAllNotes()方法如下:
@Override
public ArrayList<Note> getAllNotes() {
return (ArrayList<Note>) NoteDAO.getInstance(context).queryAllNotes();
}
再次运行测试即可通过。
-
为NoteRepository.getNote()方法编写测试用例
按照定义,NoteRepository.getNote()方法根据参数传来的id获取对应的笔记对象。这里有两层含义:
- 如果id对应的笔记对象存在,则返回这个对象
- 如果id对应的笔记对象不存在,则返回null
所以,可以编写测试用例如下:
@Test
public void testGetNote() {
// 查询不存在的记录应当返回null
assertNull(noteRepo.getNote(Long.MAX_VALUE));
Note note = new Note(0, "testGetNote", "Test 1", System.currentTimeMillis());
// 已保存的笔记对象
Note savedNote = noteRepo.saveNote(note);
long id = savedNote.getId();
// 根据id查询对应记录,应当不为null
Note result = noteRepo.getNote(id);
assertNotNull(result);
// 清除测试数据
NoteDAO.getInstance(context).deleteNoteByTitle("testGetNote");
}
立即运行此用例,继续失败。
修改NoteRepository.getNote()方法如下:
@Override
public Note getNote(long noteId) {
return NoteDAO.getInstance(context).queryNoteById(noteId);
}
再次运行测试用例,应当能够通过。
网友评论