Clean Coding Practices In .NET - Unit Tests And SOLID Principles

Writing clean and maintainable code is important for any software development project. Clean code is easy to understand, modify, and extend, while messy code can lead to bugs, technical debt, and frustrated developers. This article will discuss some best practices for writing clean code in .NET, focusing on unit tests and SOLID principles.

Unit Tests

Unit tests are automated tests that verify the behavior of individual units of code, such as methods and functions. Writing unit tests is crucial to clean coding, as they help ensure your code works as intended and catches bugs early in the development process. Here are some tips for writing effective unit tests:

Write tests for all public methods

Every public method in your code should have a corresponding unit test. This helps ensure that your code behaves correctly and catches any unexpected behavior early.

public class Calculator {
    public int Add(int a, int b) {
        return a + b;
    }
}
[TestClass]
public class CalculatorTests {
    [TestMethod]
    public void Add_ShouldReturnCorrectSum() {
        // Arrange
        Calculator calculator = new Calculator();
        int a = 2;
        int b = 3;
        // Act
        int result = calculator.Add(a, b);
        // Assert
        Assert.AreEqual(5, result);
    }
}

Test boundary conditions

Make sure to test boundary conditions, such as null inputs or extreme values, to catch any edge cases that may cause problems.

public class Validator {
    public bool IsPositive(int number) {
        return number > 0;
    }
}
[TestClass]
public class ValidatorTests {
    [TestMethod]
    public void IsPositive_ShouldReturnFalseForZero() {
            // Arrange
            Validator validator = new Validator();
            // Act
            bool result = validator.IsPositive(0);
            // Assert
            Assert.IsFalse(result);
        }
        [TestMethod]
    public void IsPositive_ShouldReturnTrueForPositiveNumber() {
        // Arrange
        Validator validator = new Validator();
        // Act
        bool result = validator.IsPositive(5);
        // Assert
        Assert.IsTrue(result);
    }
}

Use a consistent naming convention

Use a consistent naming convention for your tests, such as [MethodName]_Should[ExpectedBehavior], to make it clear what the test is testing.

Keep your tests independent

Make sure that your tests are independent of each other and don't rely on external state. This helps ensure that your tests are reliable and reproducible.

Run your tests frequently

Run your tests frequently, ideally after every code change, to catch bugs early in the development process.

SOLID Principles

SOLID is an acronym for a set of design principles that promote clean and maintainable code. Here's a brief overview of each principle:

Single Responsibility Principle (SRP)

A class should have only one reason to change. This means a class should only have one responsibility or job to do.

Open-Closed Principle (OCP)

A class should be open for extension but closed for modification. This means that you should be able to extend the behavior of a class without modifying its existing code.

Liskov Substitution Principle (LSP)

Subtypes should be substitutable for their base types. This means that any subclass or derived class should be able to be used in place of its base class without affecting the correctness of the program.

Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don't use. This means that interfaces should be as small and focused as possible.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Instead, both should depend on abstractions. This means that you should depend on abstractions, not on concrete implementations.

Here are some tips for applying SOLID principles in your .NET code,

Use dependency injection

Dependency injection is a powerful technique for applying the DIP principle. Depending on abstractions, you can decouple your code and make it more flexible and testable.

public interface ILogger {
    void Log(string message);
}
public class ConsoleLogger: ILogger {
    public void Log(string message) {
        Console.WriteLine(message);
    }
}
public class Calculator {
    private readonly ILogger _logger;
    public Calculator(ILogger logger) {
        _logger = logger;
    }
    public int Add(int a, int b) {
        int result = a + b;
        _logger.Log($ "Added {a} and {b} to get {result}");
        return result;
    }
}

Keep your classes small and focused

By following the SRP principle, you can keep your classes small and focused, which makes them easier to understand, modify, and test.

public class Calculator {
    public int Add(int a, int b) {
        return a + b;
    }
    public int Subtract(int a, int b) {
        return a - b;
    }
}

Use interfaces to define contracts

By using interfaces to define contracts between classes, you can apply the ISP principle and ensure your interfaces are as small and focused as possible.

public interface IShape {
    double CalculateArea();
}
public class Rectangle: IShape {
    public double Width {
        get;
        set;
    }
    public double Height {
        get;
        set;
    }
    public double CalculateArea() {
        return Width * Height;
    }
}
public class Circle: IShape {
    public double Radius {
        get;
        set;
    }
    public double CalculateArea() {
        return Math.PI * Math.Pow(Radius, 2);
    }
}

Favor composition over inheritance

Inheritance can lead to tight coupling and make it hard to apply the LSP principle. Instead, favor composition, where objects are composed of other objects, to make your code more flexible and extensible.

public interface IWeapon {
    void Attack();
}
public class Sword: IWeapon {
    public void Attack() {
        Console.WriteLine("Swinging sword");
    }
}
public class MagicStaff: IWeapon {
    public void Attack() {
        Console.WriteLine("Casting magic spell");
    }
}
public class Warrior {
    private readonly IWeapon _weapon;
    public Warrior(IWeapon weapon) {
        _weapon = weapon;
    }
    public void Attack() {
        _weapon.Attack();
    }
}

Conclusion

In conclusion, writing clean code is important for any software development.