構建一個ASP.NET Wiki來解釋TDD
目錄
TDD和BDD用例子解釋
介紹
在本文中,我將嘗試解釋什麼是TDD以及它在開發過程中的幫助。有很多資源和書籍可以做到這一點,但我將嘗試用一個簡單的例項介紹。這比你在書中讀到的嚴格定義更像是一個
什麼是TDD
從維基百科的定義開始:
引用:
測試驅動開發(TDD)是一個軟體開發過程,它依賴於非常短的開發週期的重複:需求變成非常具體的測試用例,然後軟體被改進以僅通過新的測試。這與軟體開發相反,軟體開發允許新增未經證明符合要求的軟體。
清楚嗎?TDD的主要目的是建立一種策略,其中測試將推動開發過程,以使編碼更有效,更高效,減少迴歸的。
先決條件是以較小的步驟分解大任務並使用單元測試進行開發。這允許您處理較小的程式碼片段,使其工作,然後將許多工作部分整合在一起。
TDD的好處
將TDD引入您的編碼體驗將達到一個轉折點。以下是最重要的好處的簡短列表:
- 專注於非常重要的一點:你將被要求分解問題,這將有助於關注最重要的事情。
- 處理更簡單的任務:每次使用單個、小型的任務可簡化故障排除並加快開發速度。你將不會陷入編寫所有程式碼的情況,然後某些東西不起作用,你不知道為什麼。
- 簡化的整合:當完成多個工作功能時,將所有功能放在一起將是一件愉快而輕鬆的任務。在迴歸的情況下,您將事先知道哪部分程式碼是壞的。
- 免費測試
TDD不是什麼
TDD是一種很好的方法,但不是:
- 替代測試(單元測試,驗收測試,UI測試)
- 你可以在一天內學會的東西
- 為你編寫程式碼的東西
- 一個神聖的人,可以驅除程式碼中的bug
TDD生命週期
TDD主要由三個步驟組成:
- 寫單元測試(RED)。
- 讓它工作(綠色)。
- 重構。
在該示例中,您可以編寫單元測試,使用其中的程式碼實現該功能,直到它工作,然後重構將這段程式碼放在需要的地方。
步驟1,2:使測試工作
public class StripTest
{
[Fact]
public static void StripHTml()
{
string test="<h1>test</h1>";
string expected="test";
string result=StripHTML(test);
Assert.Equal(expected,result);
}
public static string StripHTML(string input)
{
return Regex.Replace(input, "<.*?>", String.Empty);
}
}
第3步:重構
public class StripTest
{
[Fact]
public static void StripHTml()
{
string test="<h1>test</h1>";
string expected="test";
string result=HtmlHelper.StripHTML(test);
Assert.Equal(expected,result);
}
}
//somewhere else
public static class HtmlHelper
{
public static string StripHTML(string input)
{
return Regex.Replace(input, "<.*?>", String.Empty);
}
}
限制
在許多情況下,很難編寫涵蓋實際程式碼使用情況的單元測試。完全邏輯的過程很容易,但是當我們要涉及資料庫或UI時,編寫工作的量會增加,並且在許多情況下,可能會超過好處。有一些最佳實踐和框架對此有所幫助,但一般來說,並非應用程式的所有部分都可以使用普通單元測試進行測試。
什麼是BDD?
BDD是TDD的增強,它考慮了單元測試是限制性的情況。此擴充套件使用開發人員作為單元測試,保持BDD的理念。您仍然可以將複雜任務分解為較小的任務,使用使用者行為進行測試,和在純後端任務中使用TDD具有相同的優勢。
TDD先決條件
在團隊合作中,除了瞭解所有涉及的技術之外,所有隊友都必須瞭解並接受這一理念。
首先,您的程式碼必須由強大的單元測試系統授權:
- .NET,.NET Core:內建Visual Studio或xunit(第二個是我個人的,首選的選擇)
- Java:junit執行得很好,我不需要找到另一個解決方案
- PHP:PHP單元在所有情況下都適合我
然後,重要且必須:具有允許在測試期間模擬或重新建立正確行為的體系結構。我說的是在測試期間可以在記憶體或本地資料庫中工作的ORM,也可以使用服務或儲存庫模式。使用DI框架(內建在.NET Core中,autofac或其他任何......)也有幫助。
最後但並非最不重要:一個完善的構建過程,整合在一個持續的整合流程中,除了正確的配置之外,確定哪個單元測試在整合期間在其上執行是有意義的,以及在本地執行的內容。
例子
讓我們嘗試在現實世界的例子中實踐我們對TDD的瞭解。我想用這種方法建立一個wiki。我的意思是一個簡單的維基,使用者登入,寫下標記頁併發布。
首先,我會將“長”任務分解為較小的後續活動。每個子部分將使用小型單元測試開發。我將專注於wiki 頁面CRUD。
第1步:實體到DTO對映
- 編寫實體。
- 編寫wiki 頁面DTO。
- 編寫將實體對映到DTO的程式碼。
// Database entity
public class WikiPageEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public int Version { get; set; }
public string Slug { get; set; }
public string Body { get; set; }
public string Title { get; set; }
}
// DTO model in BLL
namespace WikiCore.Lib.DTO
{
public class WikiPageDTO
{
public string Title { get; set; }
public string BodyMarkDown { get; set; }
public string BodyHtml { get; set; }
public int Version { get; set; }
public string Slug { get; set; }
}
}
// From unit test, code omitted for brevity
public void EntityToDTO()
{
WikiPageEntity source = new WikiPageEntity()
{
Title = "title",
Slug = "titleslug",
Version =1
};
var result = Mapper.Map<wikipagedto>(source);
Assert.Equal("title", result.Title);
Assert.Equal(1, result.Version);
}
// From Mapping configuration, code omitted for brevity
public MappingProfile()
{
CreateMap<wikipageentity, wikipagedto="">().ReverseMap();
}
第2步:Markdown到HTML轉換
- 建立一個轉換markdown為HTML 的方法:
//Before refactoring public class MarkdownTest
{
[Fact]
public void ConvertMarkDown()
{
var options = new MarkdownOptions
{
AutoHyperlink = true,
AutoNewLines = true,
LinkEmails = true,
QuoteSingleLine = true,
StrictBoldItalic = true
};
Markdown mark = new Markdown(options);
var testo = mark.Transform("#testo");
Assert.Equal("<h1>testo</h1>", testo);
}
// after refactoring ( method moved to helper
[Fact]
public void ConvertMarkDownHelper()
{
Assert.Equal("<h1>testo</h1>", MarkdownHelper.ConvertToHtml("#testo"));
}
// From markdown helper
public static class MarkdownHelper
{
static MarkdownOptions options;
static Markdown converter;
static MarkdownHelper()
{
options = new MarkdownOptions
{
AutoHyperlink = true,
AutoNewLines = true,
LinkEmails = true,
QuoteSingleLine = true,
StrictBoldItalic = true
};
converter = new Markdown(options);
}
public static string ConvertToHtml(string input)
{
Markdown mark = new Markdown(options);
return mark.Transform(input);
}
}
第3步:使用Markdown進行EnHance對映
- 更改增加HTML欄位計算的對映:
// mapped profile changed
public class MappingProfile : Profile
{
public MappingProfile()
{
SlugHelper helper = new SlugHelper();
CreateMap<wikipageentity, wikipagedto="">()
.ForMember(dest => dest.BodyMarkDown, (expr) => expr.MapFrom<string>(x => x.Body))
.ForMember(dest => dest.BodyHtml,
(expr) => expr.MapFrom<string>(x => MarkdownHelper.ConvertToHtml(x.Body)))
.ReverseMap();
CreateMap<wikipagebo,wikipageentity>()
.ForMember(dest => dest.Body, (expr) => expr.MapFrom<string>(x => x.BodyMarkDown))
.ForMember(dest => dest.Slug,
(expr) => expr.MapFrom<string>(x => helper.GenerateSlug(x.Title)));
}
}
// From unit test, code omitted for brevity
public void EntityToDTO()
{
WikiPageEntity source = new WikiPageEntity()
{
Body = "# prova h1",
Title = "title",
Slug = "titleslug",
Version =1
};
var result = Mapper.Map<wikipagedto>(source);
Assert.Equal("title", result.Title);
Assert.Equal(1, result.Version);
Assert.Equal("<h1>prova h1</h1>", result.BodyHtml);
}
第4步:設定資料庫遷移
- 執行Add-Migration指令碼。
- 建立一個在記憶體中工作的單元測試來測試它。
[Fact]
public void MigrateInMemory()
{
var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
optionsBuilder.UseInMemoryDatabase();
using (var db = new DatabaseContext(optionsBuilder.Options))
{
db.Database.Migrate();
}
// No error assert migration was OK
}
第5步:實體CRUD
- 寫一個CRUD測試。
- 測試一下。
[Fact]
public void CrudInMemory()
{
var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
optionsBuilder.UseInMemoryDatabase();
using (var db = new DatabaseContext(optionsBuilder.Options))
{
db.Database.Migrate();
db.WikiPages.Add(new Lib.DAL.Model.WikiPageEntity()
{
Title = "title",
Body = "#h1",
Slug = "slug"
});
db.SaveChanges();
var count=db.WikiPages.Where(x => x.Slug == "slug").Count();
Assert.Equal(1, count);
// update, delete steps omitted for brevity
}
}
第6步:測試服務
- 使用業務邏輯建立服務。
- 測試一下。
[Fact]
public void TestSave()
{
var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
optionsBuilder.UseInMemoryDatabase();
using (var db = new DatabaseContext(optionsBuilder.Options))
{
db.Database.Migrate();
db.SaveChanges();
//this recreate same behaviour of asp.net MVC usage
DatabaseWikiPageService service = new DatabaseWikiPageService(db, Mapper.Instance);
service.Save(new Lib.BLL.BO.WikiPageBO()
{
BodyMarkDown="#h1",
Title="prova prova"
});
var item = service.GetPage("prova-prova");
Assert.NotNull(item);
}
}
第7步:繼續測試UI
一旦使用單元測試測試UI變得複雜,我就切換到BDD並完成了多個步驟來完成UI測試。因此,我不是編寫所有程式碼然後測試它,而是在多個子活動中分解問題並逐個測試:
編輯
- 準備表單,並進行測試。
- 準備模型,測試從表單提交的內容填充後端模型。
- 整合服務以儲存資料,進行測試。
檢視
- 準備模型,傳遞到檢視,測試它。
- 將模型與服務整合,以獲得真實資料。測試一下。
列表
- 準備檢視模型,將假資料傳遞給UI,測試它。
- 整合服務,測試它。
結論
TDD是一種推動測試支援的開發過程的方法。這有助於以多種方式編碼,但要求所有隊友都有一些基礎知識。一旦完成此步驟,您將處理更簡單的任務和許多可以重用的測試。如果開發時需要編寫單元測試,這個過程將有助於避免迴歸並更快地達到目標。此外,如果您的應用程式由於複雜性而難以測試,那麼您可以保持執行BDD的相同理念。
原文地址:https://www.codeproject.com/Articles/1267361/Build-an-ASP-NET-Wiki-to-Explain-TDD