[{"data":1,"prerenderedAt":980},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-unit-testing":3},{"id":4,"title":5,"body":6,"description":965,"difficulty":966,"extension":967,"framework":968,"frameworkSlug":969,"meta":970,"navigation":113,"order":86,"path":971,"qaPath":972,"seo":973,"stem":974,"subtopic":975,"topic":976,"topicSlug":977,"updated":978,"__hash__":979},"blog\u002Fblog\u002Fdotnet-unit-testing.md","Unit Testing in .NET with xUnit",{"type":7,"value":8,"toc":952},"minimark",[9,14,18,22,30,41,63,73,180,183,187,190,212,283,286,290,293,301,321,324,331,337,424,434,438,445,519,529,533,541,556,625,632,636,647,714,717,721,733,797,800,826,830,833,939,942,948],[10,11,13],"h2",{"id":12},"why-unit-testing-matters-in-net-interviews","Why unit testing matters in .NET interviews",[15,16,17],"p",{},"Interviewers care deeply about testing because it reveals how a developer thinks\nabout code quality, maintainability, and collaboration. A candidate who can write\nclear, isolated unit tests signals that they write production code that is also\neasier to reason about. This article walks through everything .NET interviewers\ncommonly probe: framework choice, structure, parameterisation, isolation, and\nthe design practices that make testing possible in the first place.",[10,19,21],{"id":20},"choosing-a-test-framework-xunit-nunit-or-mstest","Choosing a test framework: xUnit, NUnit, or MSTest",[15,23,24,25,29],{},"All three frameworks run under the same .NET test runner (",[26,27,28],"code",{},"dotnet test",") and are\nsupported by every major IDE, but they differ in philosophy and syntax.",[15,31,32,36,37,40],{},[33,34,35],"strong",{},"xUnit"," is the modern default for new projects. Its most important feature is\nthat it creates a ",[33,38,39],{},"new instance per test"," — not per class. That means instance\nfields are fresh for every test with zero effort, eliminating the most common\nsource of test pollution.",[15,42,43,46,47,50,51,54,55,58,59,62],{},[33,44,45],{},"NUnit"," is older and more feature-rich. It reuses the same instance per class\nand relies on ",[26,48,49],{},"[SetUp]","\u002F",[26,52,53],{},"[TearDown]"," for reset. Its ",[26,56,57],{},"[TestCase]"," attribute is\nslightly more compact than xUnit's ",[26,60,61],{},"[InlineData]"," for simple parameterised tests.",[15,64,65,68,69,72],{},[33,66,67],{},"MSTest"," is Microsoft's first-party framework, shipped inside Visual Studio.\nIt works identically at the conceptual level but has more boilerplate (every\nclass needs ",[26,70,71],{},"[TestClass]",").",[74,75,80],"pre",{"className":76,"code":77,"language":78,"meta":79,"style":79},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F xUnit — instance per test, no [SetUp]:\npublic class CalculatorTests\n{\n    private readonly Calculator _calc = new(); \u002F\u002F fresh every test\n\n    [Fact]\n    public void Add_TwoPositives_ReturnsSum() =>\n        Assert.Equal(5, _calc.Add(2, 3));\n\n    [Theory]\n    [InlineData(2,  3,  5)]\n    [InlineData(-1, 1,  0)]\n    [InlineData(0,  0,  0)]\n    public void Add_VariousInputs_ReturnsExpectedSum(int a, int b, int expected) =>\n        Assert.Equal(expected, _calc.Add(a, b));\n}\n","csharp","",[26,81,82,90,96,102,108,115,121,127,133,138,144,150,156,162,168,174],{"__ignoreMap":79},[83,84,87],"span",{"class":85,"line":86},"line",1,[83,88,89],{},"\u002F\u002F xUnit — instance per test, no [SetUp]:\n",[83,91,93],{"class":85,"line":92},2,[83,94,95],{},"public class CalculatorTests\n",[83,97,99],{"class":85,"line":98},3,[83,100,101],{},"{\n",[83,103,105],{"class":85,"line":104},4,[83,106,107],{},"    private readonly Calculator _calc = new(); \u002F\u002F fresh every test\n",[83,109,111],{"class":85,"line":110},5,[83,112,114],{"emptyLinePlaceholder":113},true,"\n",[83,116,118],{"class":85,"line":117},6,[83,119,120],{},"    [Fact]\n",[83,122,124],{"class":85,"line":123},7,[83,125,126],{},"    public void Add_TwoPositives_ReturnsSum() =>\n",[83,128,130],{"class":85,"line":129},8,[83,131,132],{},"        Assert.Equal(5, _calc.Add(2, 3));\n",[83,134,136],{"class":85,"line":135},9,[83,137,114],{"emptyLinePlaceholder":113},[83,139,141],{"class":85,"line":140},10,[83,142,143],{},"    [Theory]\n",[83,145,147],{"class":85,"line":146},11,[83,148,149],{},"    [InlineData(2,  3,  5)]\n",[83,151,153],{"class":85,"line":152},12,[83,154,155],{},"    [InlineData(-1, 1,  0)]\n",[83,157,159],{"class":85,"line":158},13,[83,160,161],{},"    [InlineData(0,  0,  0)]\n",[83,163,165],{"class":85,"line":164},14,[83,166,167],{},"    public void Add_VariousInputs_ReturnsExpectedSum(int a, int b, int expected) =>\n",[83,169,171],{"class":85,"line":170},15,[83,172,173],{},"        Assert.Equal(expected, _calc.Add(a, b));\n",[83,175,177],{"class":85,"line":176},16,[83,178,179],{},"}\n",[15,181,182],{},"For new projects, start with xUnit. It enforces better isolation patterns by design\nand has the most active ecosystem.",[10,184,186],{"id":185},"the-arrange-act-assert-pattern","The Arrange-Act-Assert pattern",[15,188,189],{},"Every well-structured test has exactly three phases:",[191,192,193,200,206],"ol",{},[194,195,196,199],"li",{},[33,197,198],{},"Arrange"," — prepare inputs and dependencies",[194,201,202,205],{},[33,203,204],{},"Act"," — call the one thing being tested",[194,207,208,211],{},[33,209,210],{},"Assert"," — verify the expected outcome",[74,213,215],{"className":76,"code":214,"language":78,"meta":79,"style":79},"[Fact]\npublic void PlaceOrder_WhenStockAvailable_ReturnsConfirmedOrder()\n{\n    \u002F\u002F Arrange\n    var mockInventory = new Mock\u003CIInventoryService>();\n    mockInventory.Setup(i => i.IsInStock(\"SKU-42\", 2)).Returns(true);\n    var service = new OrderService(mockInventory.Object);\n\n    \u002F\u002F Act\n    var result = service.PlaceOrder(new Order { Sku = \"SKU-42\", Quantity = 2 });\n\n    \u002F\u002F Assert\n    Assert.Equal(OrderStatus.Confirmed, result.Status);\n}\n",[26,216,217,222,227,231,236,241,246,251,255,260,265,269,274,279],{"__ignoreMap":79},[83,218,219],{"class":85,"line":86},[83,220,221],{},"[Fact]\n",[83,223,224],{"class":85,"line":92},[83,225,226],{},"public void PlaceOrder_WhenStockAvailable_ReturnsConfirmedOrder()\n",[83,228,229],{"class":85,"line":98},[83,230,101],{},[83,232,233],{"class":85,"line":104},[83,234,235],{},"    \u002F\u002F Arrange\n",[83,237,238],{"class":85,"line":110},[83,239,240],{},"    var mockInventory = new Mock\u003CIInventoryService>();\n",[83,242,243],{"class":85,"line":117},[83,244,245],{},"    mockInventory.Setup(i => i.IsInStock(\"SKU-42\", 2)).Returns(true);\n",[83,247,248],{"class":85,"line":123},[83,249,250],{},"    var service = new OrderService(mockInventory.Object);\n",[83,252,253],{"class":85,"line":129},[83,254,114],{"emptyLinePlaceholder":113},[83,256,257],{"class":85,"line":135},[83,258,259],{},"    \u002F\u002F Act\n",[83,261,262],{"class":85,"line":140},[83,263,264],{},"    var result = service.PlaceOrder(new Order { Sku = \"SKU-42\", Quantity = 2 });\n",[83,266,267],{"class":85,"line":146},[83,268,114],{"emptyLinePlaceholder":113},[83,270,271],{"class":85,"line":152},[83,272,273],{},"    \u002F\u002F Assert\n",[83,275,276],{"class":85,"line":158},[83,277,278],{},"    Assert.Equal(OrderStatus.Confirmed, result.Status);\n",[83,280,281],{"class":85,"line":164},[83,282,179],{},[15,284,285],{},"If your Act section has more than one statement, you are probably testing two\nthings at once — split the test.",[10,287,289],{"id":288},"test-naming-conventions","Test naming conventions",[15,291,292],{},"A test name should communicate what is being tested, under what condition, and\nwhat the expected outcome is. The most common pattern in .NET:",[74,294,299],{"className":295,"code":297,"language":298},[296],"language-text","MethodName_Scenario_ExpectedResult\n","text",[26,300,297],{"__ignoreMap":79},[74,302,304],{"className":76,"code":303,"language":78,"meta":79,"style":79},"public void Login_WithInvalidPassword_ThrowsUnauthorizedException() { }\npublic void GetUser_WhenUserDoesNotExist_ReturnsNull() { }\npublic void CalculateDiscount_WhenOrderOver100_Returns10Percent() { }\n",[26,305,306,311,316],{"__ignoreMap":79},[83,307,308],{"class":85,"line":86},[83,309,310],{},"public void Login_WithInvalidPassword_ThrowsUnauthorizedException() { }\n",[83,312,313],{"class":85,"line":92},[83,314,315],{},"public void GetUser_WhenUserDoesNotExist_ReturnsNull() { }\n",[83,317,318],{"class":85,"line":98},[83,319,320],{},"public void CalculateDiscount_WhenOrderOver100_Returns10Percent() { }\n",[15,322,323],{},"A well-named test reads like a specification. If the test report reads like a\nbullet-point list of the system's requirements, you have named tests well.",[10,325,327,328],{"id":326},"data-driven-tests-with-theory","Data-driven tests with ",[83,329,330],{},"Theory",[15,332,333,336],{},[26,334,335],{},"[Theory]"," eliminates copy-paste by running the same test logic against multiple\ninputs. xUnit supports three data sources:",[74,338,340],{"className":76,"code":339,"language":78,"meta":79,"style":79},"\u002F\u002F Inline — simplest; data in the attribute:\n[Theory]\n[InlineData(50,  50)]\n[InlineData(150, 135)] \u002F\u002F 10% off\npublic void Calculate_ReturnsExpectedPrice(decimal input, decimal expected)\n    => Assert.Equal(expected, new PriceCalc().Calculate(input));\n\n\u002F\u002F MemberData — for complex or reusable datasets:\npublic static IEnumerable\u003Cobject[]> EdgeCases => new[]\n{\n    new object[] { 0m,    0m   },\n    new object[] { 0.01m, 0.01m },\n};\n\n[Theory]\n[MemberData(nameof(EdgeCases))]\npublic void Calculate_EdgeCases(decimal input, decimal expected) { }\n",[26,341,342,347,352,357,362,367,372,376,381,386,390,395,400,405,409,413,418],{"__ignoreMap":79},[83,343,344],{"class":85,"line":86},[83,345,346],{},"\u002F\u002F Inline — simplest; data in the attribute:\n",[83,348,349],{"class":85,"line":92},[83,350,351],{},"[Theory]\n",[83,353,354],{"class":85,"line":98},[83,355,356],{},"[InlineData(50,  50)]\n",[83,358,359],{"class":85,"line":104},[83,360,361],{},"[InlineData(150, 135)] \u002F\u002F 10% off\n",[83,363,364],{"class":85,"line":110},[83,365,366],{},"public void Calculate_ReturnsExpectedPrice(decimal input, decimal expected)\n",[83,368,369],{"class":85,"line":117},[83,370,371],{},"    => Assert.Equal(expected, new PriceCalc().Calculate(input));\n",[83,373,374],{"class":85,"line":123},[83,375,114],{"emptyLinePlaceholder":113},[83,377,378],{"class":85,"line":129},[83,379,380],{},"\u002F\u002F MemberData — for complex or reusable datasets:\n",[83,382,383],{"class":85,"line":135},[83,384,385],{},"public static IEnumerable\u003Cobject[]> EdgeCases => new[]\n",[83,387,388],{"class":85,"line":140},[83,389,101],{},[83,391,392],{"class":85,"line":146},[83,393,394],{},"    new object[] { 0m,    0m   },\n",[83,396,397],{"class":85,"line":152},[83,398,399],{},"    new object[] { 0.01m, 0.01m },\n",[83,401,402],{"class":85,"line":158},[83,403,404],{},"};\n",[83,406,407],{"class":85,"line":164},[83,408,114],{"emptyLinePlaceholder":113},[83,410,411],{"class":85,"line":170},[83,412,351],{},[83,414,415],{"class":85,"line":176},[83,416,417],{},"[MemberData(nameof(EdgeCases))]\n",[83,419,421],{"class":85,"line":420},17,[83,422,423],{},"public void Calculate_EdgeCases(decimal input, decimal expected) { }\n",[15,425,426,427,429,430,433],{},"Use ",[26,428,61],{}," for simple literals and ",[26,431,432],{},"[MemberData]"," when the dataset is\nlarge, shared across multiple test methods, or needs its own construction logic.",[10,435,437],{"id":436},"test-isolation-and-iclassfixture","Test isolation and IClassFixture",[15,439,440,441,444],{},"xUnit's per-instance model handles basic isolation automatically. For expensive\nresources (database connections, in-memory databases) that are too slow to create\nper test, use ",[26,442,443],{},"IClassFixture\u003CT>",":",[74,446,448],{"className":76,"code":447,"language":78,"meta":79,"style":79},"public class DatabaseFixture : IDisposable\n{\n    public SqliteConnection Db { get; } = new(\"Data Source=:memory:\");\n    public DatabaseFixture() { Db.Open(); \u002F* create schema *\u002F }\n    public void Dispose()    { Db.Close(); }\n}\n\n\u002F\u002F One fixture instance shared across all tests in the class:\npublic class QueryTests : IClassFixture\u003CDatabaseFixture>\n{\n    private readonly DatabaseFixture _fx;\n    public QueryTests(DatabaseFixture fx) => _fx = fx;\n\n    [Fact] public void Query_ReturnsRows() { \u002F* uses _fx.Db *\u002F }\n}\n",[26,449,450,455,459,464,469,474,478,482,487,492,496,501,506,510,515],{"__ignoreMap":79},[83,451,452],{"class":85,"line":86},[83,453,454],{},"public class DatabaseFixture : IDisposable\n",[83,456,457],{"class":85,"line":92},[83,458,101],{},[83,460,461],{"class":85,"line":98},[83,462,463],{},"    public SqliteConnection Db { get; } = new(\"Data Source=:memory:\");\n",[83,465,466],{"class":85,"line":104},[83,467,468],{},"    public DatabaseFixture() { Db.Open(); \u002F* create schema *\u002F }\n",[83,470,471],{"class":85,"line":110},[83,472,473],{},"    public void Dispose()    { Db.Close(); }\n",[83,475,476],{"class":85,"line":117},[83,477,179],{},[83,479,480],{"class":85,"line":123},[83,481,114],{"emptyLinePlaceholder":113},[83,483,484],{"class":85,"line":129},[83,485,486],{},"\u002F\u002F One fixture instance shared across all tests in the class:\n",[83,488,489],{"class":85,"line":135},[83,490,491],{},"public class QueryTests : IClassFixture\u003CDatabaseFixture>\n",[83,493,494],{"class":85,"line":140},[83,495,101],{},[83,497,498],{"class":85,"line":146},[83,499,500],{},"    private readonly DatabaseFixture _fx;\n",[83,502,503],{"class":85,"line":152},[83,504,505],{},"    public QueryTests(DatabaseFixture fx) => _fx = fx;\n",[83,507,508],{"class":85,"line":158},[83,509,114],{"emptyLinePlaceholder":113},[83,511,512],{"class":85,"line":164},[83,513,514],{},"    [Fact] public void Query_ReturnsRows() { \u002F* uses _fx.Db *\u002F }\n",[83,516,517],{"class":85,"line":170},[83,518,179],{},[15,520,426,521,524,525,528],{},[26,522,523],{},"ICollectionFixture\u003CT>"," with ",[26,526,527],{},"[Collection(\"name\")]"," when you need the fixture\nshared across multiple test classes.",[10,530,532],{"id":531},"constructor-and-idisposable-xunits-lifecycle","Constructor and IDisposable: xUnit's lifecycle",[15,534,535,536,50,538,540],{},"xUnit deliberately omits ",[26,537,49],{},[26,539,53],{},". Instead, use the standard C#\nlifecycle:",[542,543,544,550],"ul",{},[194,545,546,549],{},[33,547,548],{},"Constructor"," — setup before each test",[194,551,552,555],{},[33,553,554],{},"IDisposable.Dispose"," — cleanup after each test",[74,557,559],{"className":76,"code":558,"language":78,"meta":79,"style":79},"public class FileTests : IDisposable\n{\n    private readonly string _tempPath = Path.GetTempFileName();\n\n    \u002F\u002F Constructor runs before each test:\n    public FileTests() => File.WriteAllText(_tempPath, \"data\");\n\n    [Fact]\n    public void Read_ReturnsContent()\n        => Assert.Equal(\"data\", File.ReadAllText(_tempPath));\n\n    \u002F\u002F Dispose runs after each test, even on failure:\n    public void Dispose() => File.Delete(_tempPath);\n}\n",[26,560,561,566,570,575,579,584,589,593,597,602,607,611,616,621],{"__ignoreMap":79},[83,562,563],{"class":85,"line":86},[83,564,565],{},"public class FileTests : IDisposable\n",[83,567,568],{"class":85,"line":92},[83,569,101],{},[83,571,572],{"class":85,"line":98},[83,573,574],{},"    private readonly string _tempPath = Path.GetTempFileName();\n",[83,576,577],{"class":85,"line":104},[83,578,114],{"emptyLinePlaceholder":113},[83,580,581],{"class":85,"line":110},[83,582,583],{},"    \u002F\u002F Constructor runs before each test:\n",[83,585,586],{"class":85,"line":117},[83,587,588],{},"    public FileTests() => File.WriteAllText(_tempPath, \"data\");\n",[83,590,591],{"class":85,"line":123},[83,592,114],{"emptyLinePlaceholder":113},[83,594,595],{"class":85,"line":129},[83,596,120],{},[83,598,599],{"class":85,"line":135},[83,600,601],{},"    public void Read_ReturnsContent()\n",[83,603,604],{"class":85,"line":140},[83,605,606],{},"        => Assert.Equal(\"data\", File.ReadAllText(_tempPath));\n",[83,608,609],{"class":85,"line":146},[83,610,114],{"emptyLinePlaceholder":113},[83,612,613],{"class":85,"line":152},[83,614,615],{},"    \u002F\u002F Dispose runs after each test, even on failure:\n",[83,617,618],{"class":85,"line":158},[83,619,620],{},"    public void Dispose() => File.Delete(_tempPath);\n",[83,622,623],{"class":85,"line":164},[83,624,179],{},[15,626,627,628,631],{},"For async setup\u002Fteardown, implement ",[26,629,630],{},"IAsyncLifetime"," instead.",[10,633,635],{"id":634},"fluentassertions-richer-failure-messages","FluentAssertions: richer failure messages",[15,637,638,639,642,643,646],{},"The built-in ",[26,640,641],{},"Assert.Equal(expected, actual)"," produces terse failure messages and\nrequires remembering argument order. ",[33,644,645],{},"FluentAssertions"," solves both:",[74,648,650],{"className":76,"code":649,"language":78,"meta":79,"style":79},"\u002F\u002F Built-in: \"Expected: 42 \u002F Actual: 43\" — you have to find the test to know what failed\nAssert.Equal(42, result);\n\n\u002F\u002F FluentAssertions: \"Expected result to be 42, but found 43\"\nresult.Should().Be(42);\n\n\u002F\u002F Collections:\norders.Should().HaveCount(3)\n               .And.AllSatisfy(o => o.Total.Should().BePositive());\n\n\u002F\u002F Exceptions:\nAction act = () => service.PlaceOrder(null!);\nact.Should().Throw\u003CArgumentNullException>().WithParameterName(\"order\");\n",[26,651,652,657,662,666,671,676,680,685,690,695,699,704,709],{"__ignoreMap":79},[83,653,654],{"class":85,"line":86},[83,655,656],{},"\u002F\u002F Built-in: \"Expected: 42 \u002F Actual: 43\" — you have to find the test to know what failed\n",[83,658,659],{"class":85,"line":92},[83,660,661],{},"Assert.Equal(42, result);\n",[83,663,664],{"class":85,"line":98},[83,665,114],{"emptyLinePlaceholder":113},[83,667,668],{"class":85,"line":104},[83,669,670],{},"\u002F\u002F FluentAssertions: \"Expected result to be 42, but found 43\"\n",[83,672,673],{"class":85,"line":110},[83,674,675],{},"result.Should().Be(42);\n",[83,677,678],{"class":85,"line":117},[83,679,114],{"emptyLinePlaceholder":113},[83,681,682],{"class":85,"line":123},[83,683,684],{},"\u002F\u002F Collections:\n",[83,686,687],{"class":85,"line":129},[83,688,689],{},"orders.Should().HaveCount(3)\n",[83,691,692],{"class":85,"line":135},[83,693,694],{},"               .And.AllSatisfy(o => o.Total.Should().BePositive());\n",[83,696,697],{"class":85,"line":140},[83,698,114],{"emptyLinePlaceholder":113},[83,700,701],{"class":85,"line":146},[83,702,703],{},"\u002F\u002F Exceptions:\n",[83,705,706],{"class":85,"line":152},[83,707,708],{},"Action act = () => service.PlaceOrder(null!);\n",[83,710,711],{"class":85,"line":158},[83,712,713],{},"act.Should().Throw\u003CArgumentNullException>().WithParameterName(\"order\");\n",[15,715,716],{},"The self-describing messages make test failures diagnose themselves — essential\nwhen CI reports failures that someone else has to triage.",[10,718,720],{"id":719},"designing-testable-code","Designing testable code",[15,722,723,724,728,729,732],{},"The most important skill tested indirectly by unit testing questions is whether\na candidate writes code that ",[725,726,727],"em",{},"can"," be tested. The key principle: ",[33,730,731],{},"make dependencies\nexplicit and injectable",".",[74,734,736],{"className":76,"code":735,"language":78,"meta":79,"style":79},"\u002F\u002F Not testable — hidden dependency, static call:\npublic class ReportService\n{\n    public string Generate() => FormatData(Database.Query(\"SELECT ...\")); \u002F\u002F untestable\n}\n\n\u002F\u002F Testable — dependency injected as an interface:\npublic class ReportService\n{\n    private readonly IDataRepository _repo;\n    public ReportService(IDataRepository repo) => _repo = repo;\n    public string Generate() => FormatData(_repo.Query(\"SELECT ...\"));\n}\n",[26,737,738,743,748,752,757,761,765,770,774,778,783,788,793],{"__ignoreMap":79},[83,739,740],{"class":85,"line":86},[83,741,742],{},"\u002F\u002F Not testable — hidden dependency, static call:\n",[83,744,745],{"class":85,"line":92},[83,746,747],{},"public class ReportService\n",[83,749,750],{"class":85,"line":98},[83,751,101],{},[83,753,754],{"class":85,"line":104},[83,755,756],{},"    public string Generate() => FormatData(Database.Query(\"SELECT ...\")); \u002F\u002F untestable\n",[83,758,759],{"class":85,"line":110},[83,760,179],{},[83,762,763],{"class":85,"line":117},[83,764,114],{"emptyLinePlaceholder":113},[83,766,767],{"class":85,"line":123},[83,768,769],{},"\u002F\u002F Testable — dependency injected as an interface:\n",[83,771,772],{"class":85,"line":129},[83,773,747],{},[83,775,776],{"class":85,"line":135},[83,777,101],{},[83,779,780],{"class":85,"line":140},[83,781,782],{},"    private readonly IDataRepository _repo;\n",[83,784,785],{"class":85,"line":146},[83,786,787],{},"    public ReportService(IDataRepository repo) => _repo = repo;\n",[83,789,790],{"class":85,"line":152},[83,791,792],{},"    public string Generate() => FormatData(_repo.Query(\"SELECT ...\"));\n",[83,794,795],{"class":85,"line":158},[83,796,179],{},[15,798,799],{},"Other testability principles:",[542,801,802,809,812,823],{},[194,803,804,805,808],{},"Avoid ",[26,806,807],{},"new","-ing dependencies inside methods — inject them",[194,810,811],{},"Prefer returning values over mutating parameters",[194,813,814,815,818,819,822],{},"Extract time (",[26,816,817],{},"IClock","), randomness (",[26,820,821],{},"IRandom","), and external I\u002FO behind interfaces",[194,824,825],{},"Keep classes small — a class with ten dependencies is nearly impossible to unit test cleanly",[10,827,829],{"id":828},"organising-large-test-suites","Organising large test suites",[15,831,832],{},"As test counts grow, organisation matters:",[74,834,836],{"className":76,"code":835,"language":78,"meta":79,"style":79},"\u002F\u002F Nested classes group tests by method:\npublic class OrderServiceTests\n{\n    public class PlaceOrder\n    {\n        [Fact] public void WhenNull_Throws() { }\n        [Fact] public void WhenOutOfStock_ReturnsRejected() { }\n    }\n\n    public class CancelOrder\n    {\n        [Fact] public void WhenPending_Succeeds() { }\n    }\n}\n\n\u002F\u002F Object Mother removes Arrange duplication:\npublic static class OrderBuilder\n{\n    public static Order Valid() => new() { Sku = \"SKU-1\", Quantity = 1 };\n    public static Order WithQuantity(int q) => Valid() with { Quantity = q };\n}\n",[26,837,838,843,848,852,857,862,867,872,877,881,886,890,895,899,903,907,912,917,922,928,934],{"__ignoreMap":79},[83,839,840],{"class":85,"line":86},[83,841,842],{},"\u002F\u002F Nested classes group tests by method:\n",[83,844,845],{"class":85,"line":92},[83,846,847],{},"public class OrderServiceTests\n",[83,849,850],{"class":85,"line":98},[83,851,101],{},[83,853,854],{"class":85,"line":104},[83,855,856],{},"    public class PlaceOrder\n",[83,858,859],{"class":85,"line":110},[83,860,861],{},"    {\n",[83,863,864],{"class":85,"line":117},[83,865,866],{},"        [Fact] public void WhenNull_Throws() { }\n",[83,868,869],{"class":85,"line":123},[83,870,871],{},"        [Fact] public void WhenOutOfStock_ReturnsRejected() { }\n",[83,873,874],{"class":85,"line":129},[83,875,876],{},"    }\n",[83,878,879],{"class":85,"line":135},[83,880,114],{"emptyLinePlaceholder":113},[83,882,883],{"class":85,"line":140},[83,884,885],{},"    public class CancelOrder\n",[83,887,888],{"class":85,"line":146},[83,889,861],{},[83,891,892],{"class":85,"line":152},[83,893,894],{},"        [Fact] public void WhenPending_Succeeds() { }\n",[83,896,897],{"class":85,"line":158},[83,898,876],{},[83,900,901],{"class":85,"line":164},[83,902,179],{},[83,904,905],{"class":85,"line":170},[83,906,114],{"emptyLinePlaceholder":113},[83,908,909],{"class":85,"line":176},[83,910,911],{},"\u002F\u002F Object Mother removes Arrange duplication:\n",[83,913,914],{"class":85,"line":420},[83,915,916],{},"public static class OrderBuilder\n",[83,918,920],{"class":85,"line":919},18,[83,921,101],{},[83,923,925],{"class":85,"line":924},19,[83,926,927],{},"    public static Order Valid() => new() { Sku = \"SKU-1\", Quantity = 1 };\n",[83,929,931],{"class":85,"line":930},20,[83,932,933],{},"    public static Order WithQuantity(int q) => Valid() with { Quantity = q };\n",[83,935,937],{"class":85,"line":936},21,[83,938,179],{},[15,940,941],{},"When three or more tests share the same Arrange block, extract a builder. Start\nwith nested classes before creating new files.",[15,943,944,947],{},[33,945,946],{},"Rule of thumb:"," Write one assertion per test, name tests like specification\nbullet points, and inject every external dependency through an interface. If you\ncannot instantiate a class in a test without a database or network, the class is\nnot designed for testing.",[949,950,951],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":79,"searchDepth":92,"depth":92,"links":953},[954,955,956,957,958,960,961,962,963,964],{"id":12,"depth":92,"text":13},{"id":20,"depth":92,"text":21},{"id":185,"depth":92,"text":186},{"id":288,"depth":92,"text":289},{"id":326,"depth":92,"text":959},"Data-driven tests with Theory",{"id":436,"depth":92,"text":437},{"id":531,"depth":92,"text":532},{"id":634,"depth":92,"text":635},{"id":719,"depth":92,"text":720},{"id":828,"depth":92,"text":829},"How to write effective unit tests in .NET — xUnit vs NUnit, the AAA structure, data-driven tests with Theory, test isolation, and the design habits that make code easy to test in the first place.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-unit-testing","\u002Fdotnet\u002Ftesting\u002Funit-testing",{"title":5,"description":965},"blog\u002Fdotnet-unit-testing","Unit Testing","Testing","testing","2026-06-23","uUlAvWv5bbsrJIyn_GBO7ujiOrYNwJJUCjJhkj1cvvc",1782244083932]