spring 中使用tdd

    科技2022-08-01  98

    spring 中使用tdd

    Test Driven Development is a controversial topic among software engineers and it is not rare to find very strong opinions in favour and against it. I am on the side of the ones in favour of it for the majority of the cases. This article is not focused on advocating in favour of TDD though, it is meant to explain how to properly implement this methodology to build an Android application.

    测试驱动开发是软件工程师中一个有争议的话题,找到非常赞成和反对的强烈见解并不罕见。 在大多数情况下,我都支持它。 本文虽然并不专注于倡导TDD,但它旨在说明如何正确实现此方法来构建Android应用程序。

    Android or more specifically the Android SDK will be just an example, if the reader decides to replace it for Angular, React, iOS SDK or even Spring or Jango, the principles and examples that will be shown are still going to be applicable. Also architecture concepts that are not specifically related to Android will be discussed, because they are needed to guarantee the tests will run fast enough for TDD to be feasible.

    Android或更具体地说是Android SDK只是一个示例,如果读者决定将其替换为Angular,React,iOS SDK甚至是Spring或Jango,将显示的原理和示例仍然适用。 此外,还将讨论与Android不特别相关的架构概念,因为它们需要保证测试能够足够快地运行以使TDD可行。

    应用范例 (Example Application)

    Throughout this article we are going to focus on an example, so it is easy for us to associate TDD with real life:

    在本文中,我们将重点放在一个示例上,因此很容易将TDD与现实生活联系起来:

    In order to organize my daily activities I want to keep a task list

    为了组织我的日常活动,我想保留一个任务列表

    空任务清单 (Empty Task List)

    Given I have no tasks yet

    鉴于我还没有任务

    When I open task application

    当我打开任务应用程序时

    Then I see Task List screen

    然后我看到“任务列表”屏幕

    And I see an empty task list

    我看到一个空的任务列表

    添加任务动作 (Add Task Action)

    Given I see the Task List screen

    鉴于我看到了“任务列表”屏幕

    When I click Add Task button

    当我单击添加任务按钮时

    Then I see Save Task screen

    然后我看到“保存任务”屏幕

    保存任务 (Save Task)

    Given I see Save Task screen

    鉴于我看到“保存任务”屏幕

    And I write call mum in the description

    我在说明中写了妈妈

    When I click Save button

    当我单击保存按钮时

    Then I see Task List screen

    然后我看到“任务列表”屏幕

    And I see call mum in the task list

    我在任务列表中看到了呼叫妈妈

    This example uses BDD to describe an application. As we can see it doesn't describe an Android, iOS or Web application, but it focus on the behaviour that should be implemented. Many times we fail to use TDD to solve a problem because we don't clearly describe the behaviour first in a way that is agnostic to technology.

    本示例使用BDD描述应用程序。 如我们所见,它没有描述Android,iOS或Web应用程序,而是集中在应实现的行为上。 很多时候,我们无法使用TDD解决问题,因为我们没有以对技术不可知的方式首先清楚地描述行为。

    TDD works best when the architecture helps to isolate technology details (like the GUI, Database, HTTP, Bluetooth etc..), because those details are slow and flaky to test automatically.

    当体系结构有助于隔离技术细节(如GUI,数据库,HTTP,蓝牙等)时,TDD最有效,因为这些细节缓慢且难以自动测试。

    Also as developers we can easily get distracted with technology and lose focus on the business value that is being added to the application. The scenarios above will help guide the creation of the tests as we will see next.

    同样,作为开发人员,我们很容易对技术产生分心,而不再关注正在添加到应用程序中的业务价值。 上面的场景将帮助指导测试的创建,我们将在接下来看到。

    测试驱动开发 (Test Driven Development)

    Create a failing test. Make it pass. Refactor.

    创建一个失败的测试。 让它通过。 重构。

    These are the fundamentals of TDD. Although there are plenty of resources to learn the basics, there is still misunderstanding around how to properly implement an application using TDD. Let's consider the following test pyramid:

    这些是TDD的基础。 尽管有很多资源可以学习基础知识,但是对于如何使用TDD正确实现应用程序仍然存在误解。 让我们考虑以下测试金字塔:

    The pyramid is about how many automated tests we will write for each category (except by Exploratory tests which are usually performed manually). Component and Integration tests are the most misunderstood ones, therefore applications tend to have only Unit and/or End-to-end tests. By Martin Fowler's definition in Testing Strategies in a Microservices Architecture a Component test integrates all the parts of an application that are not slow. Slow parts are external services accessed via I/O operations like a database or a http server.

    金字塔是关于我们将为每个类别编写的自动化测试数量(通常由手动执行的探索性测试除外)。 组件和集成测试是最容易被误解的测试,因此应用程序倾向于仅进行单元和/或端到端测试。 根据Martin Fowler在“微服务体系结构中的测试策略”中的定义, 组件测试集成了应用程序中不慢的所有部分。 慢速部分是通过I / O操作(如数据库或http服务器)访问的外部服务。

    Integration tests check whether our application works with external services by calling them, what can be slow and flaky. We can design our code so Integration tests focus on verifying that our application respects the contract defined by these services. There are various ways to specify the contract, but commonly it is specified via SQL in case of a database or via a URL and JSON in case of a http server.

    集成测试通过调用外部服务来检查我们的应用程序是否可与外部服务一起使用,这是缓慢而脆弱的。 我们可以设计代码,因此集成测试专注于验证我们的应用程序是否遵守这些服务定义的合同。 有多种指定合同的方法,但是通常在数据库的情况下通过SQL进行指定,在http服务器的情况下通过URL和JSON进行指定。

    自上而下与自下而上 (Top-Down vs. Bottom-Up)

    When approaching a new feature it is a common mistake to implement TDD beginning at the bottom of the test pyramid. This is not a good idea because TDD is essentially a design methodology, since it helps us to find how to solve a problem as we add new tests. In order to use a bottom-up approach we need to make a design decision first and then start building on top of that decision.

    接近新功能时,从测试金字塔的底部开始实施TDD是一个常见的错误。 这不是一个好主意,因为TDD本质上是一种设计方法,因为它可以帮助我们在添加新测试时找到解决问题的方法。 为了使用自下而上的方法,我们需要先做出设计决策,然后再基于该决策开始构建。

    For example in the Empty Task List scenario a bottom-up approach could start by creating a getTasks method in TaskPersistence interface and then create a TaskSQLiteDatabase implementation. Doing that by writing the tests first is possible, but that wouldn't really be TDD, because we started by making a design decision. We decided to create a separated class to handle the persistence of the tasks. Also we decided to create an abstraction with TaskPersistence interface and that a SQLite database would be used to persist the tasks.

    例如,在“ 空任务列表”方案中,自下而上的方法可以从在TaskPersistence接口中创建getTasks方法开始,然后创建TaskSQLiteDatabase实现。 可以通过首先编写测试来做到这一点,但这并不是TDD,因为我们首先要做出设计决策。 我们决定创建一个单独的类来处理任务的持久性。 我们还决定使用TaskPersistence接口创建一个抽象,并且将使用SQLite数据库来持久化任务。

    Although some of these decisions will be made at a certain point in time, with TDD we should make them motivated by the behaviour we want to implement. This approach avoids accidental complexity because it is very easy to make mistakes when we create abstractions up-front. TDD is about "forgetting" our past experiences (architecture approaches, past projects and past mistakes) and letting the tests guide us. When we reach the Refactor phase we can apply our experience by aiming to be as lean as possible.

    尽管其中一些决策将在某个特定的时间点做出,但是使用TDD时,我们应该使它们受到我们要实现的行为的激励。 这种方法避免了意外的复杂性,因为当我们预先创建抽象时,很容易犯错误。 TDD旨在“忘记”我们过去的经验(架构方法,过去的项目和过去的错误),并让测试指导我们。 当我们进入重构阶段时,我们可以通过尽可能精简来运用我们的经验。

    In a top-down approach we would start at the top of the pyramid (or as close as we can to the user) and then derive the rest of the application from it. Since we are going to use TDD we shouldn't start with Exploratory tests, because they are manual. Also End-to-end are not the best option, because they are flaky and take too much time to run. Starting with End-to-end tests would make our feedback loop too long. The Component tests are the best starting point, because we start from the perspective of the user and still can have a short feedback loop.

    在自顶向下的方法中,我们将从金字塔的顶部开始(或尽可能接近用户),然后从金字塔的顶部导出应用程序的其余部分。 由于我们将使用TDD,因此我们不应该从探索性测试入手,因为它们是手动的。 此外, 端到端也不是最佳选择,因为它们易碎且运行时间过多。 从端到端测试开始会使反馈循环太长。 组件测试是最好的起点,因为我们是从用户的角度出发的,并且仍然会有短暂的反馈循环。

    图形用户界面 (Graphic User Interface)

    In Android the GUI can be tested with instrumentation tests using Espresso which requires an emulator, what makes them slow and flaky. If we use Robolectric the tests run much faster, but not as fast as if they were written in pure Kotlin. Robolectric sets up a fake Android SDK in the local JVM when the tests run, which increases the tests run time. In a Web application using React it wouldn’t be the case, because React tests run quite fast we could easily write tests from the GUI level.

    在Android中,可以使用Espresso对Espresso进行工具测试,以测试GUI,这使它们运行缓慢且不稳定。 如果我们使用Robolectric,则测试的运行速度要快得多,但不如用纯Kotlin编写的速度快。 测试运行时,Robolectric在本地JVM中设置了伪造的Android SDK,这增加了测试运行时间。 在使用React的Web应用程序中,情况并非如此,因为React测试运行得非常快,我们可以轻松地从GUI级别编写测试。

    Since only the happy path will be tested through the GUI, Robolectric is fast enough to allow us to test drive an Android application. Most of the tests will be implemented in different layers of the test pyramid anyways, leaving the GUI out.

    由于只有幸福的道路将通过GUI进行测试,因此Robolectric足够快,可以让我们测试驱动Android应用程序。 无论如何,大多数测试将在测试金字塔的不同层中实施,而将GUI排除在外。

    让我们编码吧! (Let's code!)

    So we will start the development of the story above. Save Task scenario is the one that brings value to the user, once it is completed the story will be done. The problem is we have a greenfield application and starting with Save Task would make our feedback loop too long.

    因此,我们将开始上述故事的发展。 “保存任务”方案是为用户带来价值的方案,一旦完成,便会完成故事。 问题是我们有一个未开发的应用程序,从“ 保存任务”开始会使反馈循环太长。

    Add Task Action and Empty Task List scenarios are much simpler. It is more natural to implement Empty Task List scenario first, since it is the starting state of the application and then Add Task Action scenario. Empty Task List is an edge case, it doesn't bring that much value to the user, therefore I will test-drive its implementation without the GUI. Also we should keep edge case scenarios in lower levels of the test pyramid, leaving the happy path scenarios in higher levels.

    添加任务操作和清空任务列表方案要简单得多。 首先实现Empty Task List方案是更自然的,因为它是应用程序的启动状态,然后是Add Task Action方案。 空任务列表是一个极端的案例,它不会为用户带来太多价值,因此我将在没有GUI的情况下测试其实现。 同样,我们应将边缘案例方案保留在测试金字塔的较低级别中,而将快乐路径方案保留在较高级别中。

    @Test fun `Given I have no tasks yet When I open task application Then I see Task List screen And I see no tasks`() { val taskApplication = TaskApplication() taskApplication.open() taskApplication.withScreenCallback { screen -> assertEquals(emptyList<String>(), screen.tasks) } }

    Since this test keeps the UI out, some design decisions have to be made while writing it. A callback is registered in TaskApplication to receive TaskListScreen with the list of tasks.

    由于此测试将UI排除在外,因此在编写UI时必须做出一些设计决策。 在TaskApplication中注册了一个回调,以接收带有任务列表的TaskListScreen 。

    class TaskApplication { private lateinit var screen: Screen fun open() { screen = TaskListScreen(emptyList()) } fun withScreenCallback(callback: (TaskListScreen) -> Unit) { callback.invoke(screen) } } data class TaskListScreen( val tasks: List<String> )

    As we see the minimal possible changes were made to make the test pass. Let's tackle Add Task Action scenario next:

    如我们所见,为了通过测试,做出了最小的更改。 接下来处理添加任务操作方案:

    @Test fun `Given I see Task List screen When I tap add task Then I see Save Task screen`() { val taskApplication = TaskApplication() taskApplication.open() taskApplication.addTask() taskApplication.withScreenCallback { screen -> assertEquals(true, screen is SaveTaskScreen) } }

    We had to add a Screen interface to create this test, in order to reuse withScreenCallback method for SaveTaskScreen.

    我们不得不添加屏幕界面来创建这个测试中,为了重用withScreenCallback方法SaveTaskScreen。

    class TaskApplication { private var screenCallback: ((Screen) -> Unit)? = null private lateinit var screen: Screen fun open() { screen = TaskListScreen(emptyList()) } fun withScreenCallback(callback: (Screen) -> Unit) { this.screenCallback = callback this.screenCallback?.invoke(screen) } fun addTask() { screen = SaveTaskScreen() this.screenCallback?.invoke(screen) } } interface Screen data class TaskListScreen( val tasks: List<String> ) : Screen class SaveTaskScreen: Screen

    Now we also can apply some refactors. The screenCallback variable can have a default no action value, then we can avoid the ? syntax and call the function directly. Also we can move Screen, TaskListScreen and SaveTaskScreen to their respective files.

    现在我们还可以应用一些重构。 screenCallback变量可以具有默认的no action值,那么我们可以避免使用? 语法并直接调用该函数。 我们也可以将Screen , TaskListScreen和SaveTaskScreen移到它们各自的文件中。

    class TaskApplication { private object NoActionScreenCallback : (Screen) -> Unit { override fun invoke(screen: Screen) {} } private var screenCallback : (Screen) -> Unit = NoActionScreenCallback private lateinit var screen: Screen fun open() { screen = TaskListScreen(emptyList()) } fun withScreenCallback(callback: (Screen) -> Unit) { this.screenCallback = callback this.screenCallback(screen) } fun addTask() { screen = SaveTaskScreen() this.screenCallback(screen) } }

    Finally we can attack the Save Task scenario. Now we are going to add the UI to the test, making it more complete.

    最后,我们可以攻击“ 保存任务”方案。 现在,我们将UI添加到测试中,使其更加完整。

    @RunWith(AndroidJUnit4::class) class TaskManagerTest { @Test fun `Given I have no tasks And I see Save Task screen And I fill description When I tap Save button Then I see Task List screen with description`() { val scenario = ActivityScenario.launch(MainActivity::class.java) val addTask = withId(R.id.view_task_list_add_task_button) val description = withId(R.id.view_add_task_description_input_field) val saveTask = withId(R.id.view_add_task_save_task_button) onView(addTask).perform(click()) onView(description).perform(replaceText("My description")) onView(saveTask).perform(click()) onView(withText("My description")).check(matches(isDisplayed())) scenario.close() } }

    This test fails, because we don't have a MainActivity yet, and it is not declared in the AndroidManifest. Also the ids we are referencing don't exist. To make it pass some design decisions have to be made. Since there will two screens in the application we could use Fragments, Activities or Views to represent them. I will use Views and a single Activity, for personal preference.

    该测试失败,因为我们还没有MainActivity ,并且未在AndroidManifest中声明。 同样,我们引用的ID不存在。 为了使其通过,必须做出一些设计决策。 由于应用程序中将有两个屏幕,因此我们可以使用Fragments , Activity或Views来表示它们。 我将根据个人喜好使用Views和单个Activity 。

    class MainApplication : Application() { val taskApplication by lazy { TaskApplication() } override fun onCreate() { super.onCreate() taskApplication.open() } } class MainActivity : AppCompatActivity() { private val taskApplication by lazy { (application as MainApplication).taskApplication } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val frame = findViewById<FrameLayout>(R.id.activity_main_frame) taskApplication.withScreenCallback { screen -> frame.removeAllViews() when (screen) { is TaskListScreen -> { val view = TaskListView(this) view.application = taskApplication frame.addView(view) view.updateScreen(screen) } is SaveTaskScreen -> { val view = SaveTaskView(this) view.application = taskApplication frame.addView(view) } } } } } class TaskListView : ConstraintLayout { lateinit var application: TaskApplication private lateinit var listView: RecyclerView constructor(context: Context) : super(context) { init() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init() } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() } fun updateScreen(screen: TaskListScreen) { (listView.adapter as TaskListAdapter).updateTasks(screen.tasks) } private fun init() { inflate(context, R.layout.view_task_list, this) findViewById<Button>(R.id.view_task_list_add_task_button).setOnClickListener { application.addTask() } listView = findViewById(R.id.view_task_list_view) listView.layoutManager = LinearLayoutManager(context); listView.adapter = TaskListAdapter(LayoutInflater.from(context)) } } class SaveTaskView : ConstraintLayout { lateinit var application: TaskApplication lateinit var screen: SaveTaskScreen private lateinit var saveTaskButton: Button private lateinit var descriptionInputField: EditText constructor(context: Context) : super(context) { init() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init() } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() } private fun init() { inflate(context, R.layout.view_add_task, this) saveTaskButton = findViewById(R.id.view_add_task_save_task_button) descriptionInputField = findViewById(R.id.view_add_task_description_input_field) saveTaskButton.setOnClickListener { application.saveTask(descriptionInputField.text.toString()) } } } class TaskApplication { private object NoActionScreenCallback : (Screen) -> Unit { override fun invoke(screen: Screen) {} } private var screenCallback : (Screen) -> Unit = NoActionScreenCallback private lateinit var screen: Screen private val tasks = mutableListOf<String>() fun open() { screen = TaskListScreen(tasks) } fun withScreenCallback(callback: (Screen) -> Unit) { this.screenCallback = callback this.screenCallback(screen) } fun addTask() { screen = SaveTaskScreen() this.screenCallback(screen) } fun saveTask(description: String) { tasks.add(description) screen = TaskListScreen(tasks) this.screenCallback(screen) } }

    In order to make the test pass I had to create many classes (I have omitted the xml files and the list adapter implementation). The feedback loop time was not ideal, but the next stories will have less code associated because the foundations will already be in place. In order to mitigate a long feedback loop in greenfield applications the first story has to be made as small as possible, while still providing value to the user.

    为了使测试通过,我必须创建许多类(我省略了xml文件和列表适配器实现)。 反馈循环时间并不理想,但由于基础已经存在,因此下一个故事的代码关联较少。 为了减轻未开发应用中的长反馈回路,必须使第一个故事尽可能小,同时仍为用户提供价值。

    A tasks variable was created in TaskApplication, which means the tasks are kept in the memory. If the user kills the application and reopens it the tasks will be gone. We need to make sure the tasks are persisted, therefore we need a new scenario:

    在TaskApplication中创建了一个task变量,这意味着任务被保存在内存中。 如果用户杀死该应用程序并重新打开它,则任务将消失。 我们需要确保任务是持久的,因此我们需要一个新方案:

    坚持保存的任务 (Persist Saved Tasks)

    Given I save call mum task

    给我保存通话妈妈任务

    And I close task application

    我关闭任务申请

    When I open task application

    当我打开任务应用程序时

    Then I see call mum task in the task list

    然后我在任务列表中看到call mum任务

    In an agile team it is very normal for scenarios to be added (and removed) while the story is in development. It is impossible and counter-productive to try to think of all scenarios at the beginning. As the story evolves Designers, QA, Stakeholders and Engineers will come up with new scenarios. As long as we focus on the minimum viable product this is fine. In our case it doesn't make sense for the user if the application doesn't persist the tasks.

    在敏捷团队中,在故事发展过程中添加(和删除)场景是很正常的。 在一开始尝试所有场景都是不可能的,而且适得其反。 随着故事的发展,设计师,质量保证,利益相关者和工程师将提出新的方案。 只要我们专注于最低限度的可行产品,就可以了。 在我们的情况下,如果应用程序不执行任务,那么对用户来说就没有意义。

    @Test fun `Given I have a task And I close the application When I open task application Then I see Task List screen And I see the task`() { val taskApplication = TaskApplication() taskApplication.open() taskApplication.addTask() taskApplication.saveTask("Clean up my room") val newTaskApplication = TaskApplication() newTaskApplication.open() newTaskApplication.withScreenCallback { screen -> assertEquals(true, screen is TaskListScreen) assertEquals(listOf("Clean up my room"), (screen as TaskListScreen).tasks) } }

    The test is implemented without the UI, because this is an edge case and also because it is way easier to simulate the scenario.

    该测试是在没有UI的情况下实现的,因为这是一个极端的情况,而且还因为它更容易模拟场景。

    class TaskApplication(private val context: Context) { object NoActionScreenCallback : (Screen) -> Unit { override fun invoke(screen: Screen) {} } private var screenCallback : (Screen) -> Unit = NoActionScreenCallback private lateinit var screen: Screen private val tasks = mutableListOf<String>() private val taskHandler by lazy { val thread = HandlerThread("task") thread.start() Handler(thread.looper) } private val mainHandler by lazy { Handler(Looper.getMainLooper()) } private val database by lazy { SQLiteOpenHelperDatabase( context, "task_database", 1, "CREATE TABLE IF NOT EXISTS tasks (description TEXT)") } fun open() { taskHandler.post { var cursor: Cursor? = null try { cursor = database .readableDatabase .rawQuery("SELECT * FROM tasks", null) while (cursor.moveToNext()) { tasks.add(cursor.getString(cursor.getColumnIndex("description"))) } } finally { cursor?.close() } updateScreen(TaskListScreen(tasks)) } } fun saveTask(description: String) { taskHandler.post { database .writableDatabase .execSQL("INSERT INTO tasks (description) VALUES ('$description')") tasks.add(description) updateScreen(TaskListScreen(tasks)) } } fun withScreenCallback(callback: (Screen) -> Unit) { taskHandler.post { this.screenCallback = callback updateScreen(screen) } } fun addTask() { taskHandler.post { updateScreen(SaveTaskScreen()) } } private fun updateScreen(screen: Screen) { this.screen = screen mainHandler.post { screenCallback(this.screen) } } } class SQLiteOpenHelperDatabase( context: Context, name: String?, version: Int, private val createSQL: String ) : SQLiteOpenHelper(context, name, null, version) { override fun onCreate(database: SQLiteDatabase) { database.execSQL(createSQL) } override fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) { } }

    I have chosen to use Handler to implement asynchronous behaviour and to post back to the main thread. Posting to taskHandler in every public method ensures asynchronous actions occur sequentially (avoiding inconsistent states). Also SQLite was chosen to persist the tasks. Although this implementation makes the test pass, it is not very clean, so let's refactor it to separate Handler and SQLite dependencies away from our application logic.

    我选择使用Handler来实现异步行为并将其发布回主线程。 在每个公共方法中发布到taskHandler可以确保异步操作顺序发生(避免出现不一致的状态)。 还选择了SQLite来保留任务。 尽管此实现使测试通过,但它不是很干净,因此让我们对其进行重构,以将Handler和SQLite依赖项与应用程序逻辑分开。

    class TaskRepository(context: Context) { private val database by lazy { SQLiteOpenHelperDatabase( context, "task_database", 1, "CREATE TABLE IF NOT EXISTS tasks (description TEXT)") } fun getTasks(): List<String> { val tasks = mutableListOf<String>() var cursor: Cursor? = null try { cursor = database .readableDatabase .rawQuery("SELECT * FROM tasks", null) while (cursor.moveToNext()) { tasks.add(cursor .getString(cursor.getColumnIndex("description"))) } } finally { cursor?.close() } return tasks } fun saveTask(description: String) { database .writableDatabase .execSQL("INSERT INTO tasks (description) VALUES ('$description')") } } interface Scheduler { fun schedule(action: () -> Unit) } class HandlerScheduler(private val name: String) : Scheduler { private val handler by lazy { val handlerThread = HandlerThread(name) handlerThread.start() Handler(handlerThread.looper) } override fun schedule(action: () -> Unit) { handler.post { action() } } } object MainHandlerScheduler: Scheduler { private val mainHandler by lazy { Handler(Looper.getMainLooper()) } override fun schedule(action: () -> Unit) { mainHandler.post { action() } } } class TaskApplication( context: Context, private val repository: TaskRepository = TaskRepository(context), private val scheduler: Scheduler = HandlerScheduler("task"), private val mainScheduler: Scheduler = MainHandlerScheduler) { private object NoActionScreenCallback : (Screen) -> Unit { override fun invoke(screen: Screen) {} } private var screenCallback : (Screen) -> Unit = NoActionScreenCallback private lateinit var screen: Screen private val tasks = mutableListOf<String>() fun open() { scheduler.schedule { tasks.addAll(repository.getTasks()) updateScreen(TaskListScreen(tasks)) } } fun withScreenCallback(callback: (Screen) -> Unit) { scheduler.schedule { this.screenCallback = callback updateScreen(screen) } } fun addTask() { scheduler.schedule { updateScreen(SaveTaskScreen()) } } fun saveTask(description: String) { scheduler.schedule { repository.saveTask(description) tasks.add(description) updateScreen(TaskListScreen(tasks)) } } private fun updateScreen(screen: Screen) { this.screen = screen mainScheduler.schedule { screenCallback(this.screen) } } }

    Now when we read TaskApplication code we can focus on the business logic. There would be some other scenarios related with back navigation and possible memory leaks in MainActivity (because we currently don't remove the callback in onDestroy) that should be implemented using Unit tests.

    现在,当我们阅读TaskApplication代码时,我们可以专注于业务逻辑。 还会有其他场景与后向导航以及MainActivity中可能的内存泄漏(因为我们当前不删除onDestroy中的回调)有关,这些场景应该使用单元测试来实现。

    Edge cases related to errors are good candidates to be implemented using Unit tests as well, because setting up a Component test for them would be cumbersome. To simulate non-standard behaviours in a Component test requires the injection of mocks or fakes, which makes the test white box. Component tests have to be black box so after a big refactor of the application, we can run them and feel confident the system still works as expected.

    与错误相关的边缘情况也是使用单元测试实现的很好的候选者,因为为其设置组件测试将很麻烦。 要在组件测试中模拟非标准行为,需要注入模拟或伪造品,这使测试成为白盒。 组件测试必须是黑匣子,因此在对应用程序进行大量重构之后,我们可以运行它们,并确信系统仍然可以按预期运行。

    Since Robolectric has an option to run SQLite database in-memory, our tests run against the real database implementation, therefore we don't need to write Integration tests. If we were querying the tasks via HTTP then we would need to fake the server. In that case an Integration test must be written to validate that the server behaves as expected. For example in our Integration test, we would instantiate TaskRepository (that would talk to a server via HTTP instead of to a database via SQLite) and call its methods. If the values returned by TaskRepository correspond to the data in the server under test, the test would pass.

    由于Robolectric可以选择在内存中运行SQLite数据库,因此我们的测试针对实际的数据库实现运行,因此我们不需要编写Integration测试。 如果我们通过HTTP查询任务,则需要伪造服务器。 在这种情况下,必须编写集成测试以验证服务器的行为是否符合预期。 例如,在集成测试中,我们将实例化TaskRepository (它将通过HTTP与服务器通信而不是通过SQLite与数据库通信)并调用其方法。 如果TaskRepository返回的值与被测服务器中的数据相对应,则测试将通过。

    结论 (Conclusion)

    TDD is about navigating the test pyramid and deciding in which layer a behaviour should be implemented. In order to do that, first the behaviour must be very clear. We should favor implementing a new behaviour at the top of the test pyramid, so we have all the happy paths covered by integrating everything that is not slow in our application.

    TDD涉及导航测试金字塔并确定应在哪一层实施行为。 为此,首先必须明确行为。 我们应该倾向于在测试金字塔的顶部实现新的行为,因此,通过集成应用程序中所有不慢的内容,我们拥有了所有快乐的道路。

    By following this approach we can avoid accidental complexity and keep ourselves focused on adding value to the user. Also the development speed remains stable over time as the source code scales, because coverage gives developers confidence to change the application.

    通过遵循这种方法,我们可以避免意外的复杂性,并使自己专注于为用户增加价值。 随着源代码的扩展,随着时间的推移,开发速度也会保持稳定,因为覆盖率使开发人员有信心更改应用程序。

    翻译自: https://medium.com/swlh/tdd-in-android-d0347c944a9a

    spring 中使用tdd

    相关资源:微信小程序源码-合集6.rar
    Processed: 0.010, SQL: 8