NUnit的Setup属性是否为代码异味?共享初始化代码该如何处理?
Great question—this is such a common tension between keeping tests maintainable (via DRY) and making them self-documenting, so you’re not alone in wrestling with this!
Let’s break this down:
First, acknowledge both sides of the argument
- James Newkirk’s point about scrolling is totally valid: when reading a test, you shouldn’t have to jump back and forth to the
SetUpmethod to understand what the starting state is. Tests should ideally be self-contained so anyone can read a single test method and grasp its context quickly. - But your DRY concern is also spot-on: repeating the same initialization code across every test is messy, hard to update, and invites typos. No one wants to change a Product constructor parameter in 10 different test methods!
So what’s the middle ground?
It depends on your specific test suite, but here are some practical approaches:
1. Keep SetUp if all tests truly share identical, unmodified state
If every single test in your class uses _product1, _product2, and _products in exactly their default initialized state (no test modifies these objects), then SetUp is perfectly reasonable. To mitigate the readability issue:
- Give your fields clear, descriptive names (you’re already doing this—great!)
- Add a brief comment above the fields to explain their default state, so readers don’t have to jump to
SetUp:// Initialized in SetUp: two default Product instances in a list private Product _product1; private Product _product2; private IList<Product> _products;
2. Use factory methods for flexible, readable initialization
If some tests only need a subset of these objects, or if tests modify the state of the objects (e.g., setting a Product’s Price), ditching SetUp for factory methods is a better call. This keeps DRY while making each test’s context explicit:
// Reusable factory methods private Product CreateDefaultProduct() => new Product(); private IList<Product> CreateDefaultProductList() { var product1 = CreateDefaultProduct(); var product2 = CreateDefaultProduct(); return new List<Product> { product1, product2 }; } // Example test [Test] public void AddingProduct_IncreasesListCount() { var products = CreateDefaultProductList(); var newProduct = CreateDefaultProduct(); products.Add(newProduct); Assert.That(products.Count, Is.EqualTo(3)); }
Now anyone reading the test can immediately see exactly what objects are being used, without scrolling to SetUp.
3. Avoid shared state that changes between tests
Even though NUnit creates a new instance of your test class for every test (so SetUp runs fresh each time), if your tests modify the shared objects, it can still create confusion for readers. If a test needs a modified Product (e.g., with a specific ID), initialize that directly in the test or create a specialized factory method like CreateProductWithId(int id).
Final takeaway
There’s no one-size-fits-all answer, but the priority should be test readability first, DRY second. Tests act as living documentation for your code, so making them easy to understand should outweigh avoiding a little duplication. That said, factory methods are usually the sweet spot—they keep code DRY while keeping each test self-contained.
内容的提问来源于stack exchange,提问作者w0051977

