TDD和重構遇到的困難(或者-為什麼這比應該的要痛苦得多?)


20

我想教自己使用TDD方法,並且我有一個項目想要進行一段時間。這不是一個大項目,所以我認為這將是TDD的不錯的選擇。但是,我感覺有些不對勁。讓我舉個例子:

總的來說,我的項目是Microsoft OneNote的加載項,它使我可以更輕鬆地跟踪和管理項目。現在,如果我決定建立自己的自定義存儲和後端的一天,我還希望保持與OneNote分離的業務邏輯。

首先,我從一個基本的普通單詞接受測試開始,概述了我希望我的第一個功能要做的事情。看起來像這樣(為了簡潔起見,將其複制):

  1. 用戶點擊創建項目
  2. 項目標題中的用戶類型
  3. 驗證項目是否正確創建

跳過UI內容和一些中間計劃,我來進行第一次單元測試:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

到目前為止一切順利。紅色,綠色,重構等。現在它實際上需要保存內容。在這裡刪節一些步驟,我結束了。

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

我現在仍然感覺很好。我還沒有具體的數據存儲,但是我按預期的方式創建了界面。

由於這篇文章已經足夠長了,因此我將在此處跳過一些步驟,但是我遵循了類似的過程,最終我對數據存儲進行了此測試:

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

這很好,直到我嘗試實現它:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

" ..."所在的位置就是問題所在。現在,我意識到CreatePage需要一個部分ID。當我在控制器級別進行思考時,我並沒有意識到這一點,因為我只關心測試與控制器相關的位。但是,直到現在,我一直意識到我必須要問用戶一個存儲項目的位置。現在,我必須將位置ID添加到數據存儲中,然後將其添加到項目中,然後將其添加到控制器中,然後將其添加到已經針對所有這些內容編寫的所有測試中。它變得非常繁瑣,我不禁感到,如果我提前草擬設計而不是在TDD流程中進行設計,我會更快地抓住這一點。

如果我在此過程中做錯了什麼,可以給我解釋一下嗎?無論如何,可以避免這種重構嗎?還是這很常見?如果很常見,有什麼方法可以使它更輕鬆嗎?

謝謝!

19

While TDD is (rightly) touted as a way to design and grow your software, it's still a good idea to think about the design and architecture beforehand. IMO, "sketching out the design ahead of time" is fair game. Often this will be at a higher level than the design decisions you will be led to through TDD, however.

It's also true that when things change, you will usually have to update tests. There's no way to eliminate this completely, but there are some things you can do to make your tests less brittle and minimize the pain.

  1. As much as possible, keep implementation details out of your tests. This means only test through public methods, and where possible favor state-based over interaction-based verification. In other words, if you test the result of something rather than the steps to get there, your tests should be less fragile.

  2. Minimize duplication in your test code, just like you would in production code. This post is a good reference. In your example, it sounds like it was painful to add the ID property to your constructor because you invoked the constructor directly in several different tests. Instead, try extracting the creation of the object to a method or initializing it once for each test in a test initialize method.


10

...I can't help but feel like I would have caught this quicker if I sketched out the design ahead of time rather than letting it be designed during the TDD proces...

Maybe, maybe not

On the one hand, TDD worked just fine, giving you automated tests as you built up functionality, and immediately breaking when you had to change the interface.

On the other hand, perhaps if you had started with the high-level feature (SaveProject) instead of a lower-level feature (CreateProject), you would have noticed missing parameters sooner.

Then again, maybe you wouldn't have. It's an unrepeatable experiment.

But if you're looking for a lesson for next time: start at the top. And think about the design as much as you want first.


0

https://frontendmasters.com/courses/angularjs-and-code-testability/ From about 2:22:00 to the end (about 1 hour). Sorry that the video isn't free, but I haven't found a free one that explains it so well.

One of the best presentations of writing testable code is in this lesson. It's a AngularJS class, but the testing part is all around java code, primarily because what he is talking about has nothing to do with the language, and everything to do with writing good testable code in the first place.

The magic is in writing testable code, rather than writing code tests. It's not about writing code that pretends to be a user.

He also spends some time writing the spec in the form of test assertions.