Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Unit testing and SOLID principles: Best practices

April 11, 2023

Unit testing and SOLID principles: Best practices

In the software development world, unit testing and SOLID principles play a crucial role in ensuring the quality, maintainability, and extensibility of the codebase. In this blog post, we will explore the best practices of unit testing and the application of SOLID principles with extensive examples in C and Python.

Understanding Unit Testing

Unit testing is the process of testing individual units or components of a software application in isolation. It involves writing test cases for functions, methods, or classes to verify that they work as expected. Unit testing helps in identifying bugs and issues early in the development process and provides a safety net for refactoring and modifying code.

Applying SOLID Principles

SOLID is an acronym for five design principles that help developers create maintainable, scalable, and robust software. The principles are:

Now, let’s dive into the best practices and examples of unit testing and SOLID principles in C and Python.

Best Practices and Examples

Single Responsibility Principle (SRP)

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Bad design violating SRP
void processUserDataAndSaveToFile(char* userData) {
    // Process user data (e.g., validate, sanitize)
    // Save data to a file
}

// Better design following SRP
void processUserData(char* userData) {
    // Process user data (e.g., validate, sanitize)
}

void saveDataToFile(char* data) {
    // Save data to a file
}

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Bad design violating SRP
class UserDataHandler:
    def processUserDataAndSaveToFile(self, userData):
        # Process user data (e.g., validate, sanitize)
        # Save data to a file

# Better design following SRP
class UserDataProcessor:
    def processUserData(self, userData):
        # Process user data (e.g., validate, sanitize)

class DataFileSaver:
    def saveDataToFile(self, data):
        # Save data to a file

Open/Closed Principle (OCP)

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Applying OCP using function pointers
typedef void (*Operation)(int, int);

void add(int a, int b) {
    printf("Sum: %d\n", a + b);
}

void subtract(int a, int b) {
    printf("Difference: %d\n", a - b);
}

void executeOperation(Operation op, int a, int b) {
    op(a, b);
}

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Applying OCP using classes and inheritance
class Operation:
    def operate(self, a, b):
        pass

class Addition(Operation):
    def operate(self, a, b):
        print(f"Sum: {a + b}")

class Subtraction(Operation):
    def operate(self, a, b):
        print(f"Difference: {a - b}")

def executeOperation(operation, a, b):
    operation.operate(a, b)

Liskov Substitution Principle (LSP)

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Violating LSP
void processRectangle(Rectangle r) {
    // Process rectangle
}

// Substitutability violated
void processSquare(Square s) {
    // Process square (intended)
}

// Better design respecting LSP
void processShape(Shape* shape) {
    shape->process();
}

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Violating LSP
class Rectangle:
    def process(self):
        # Process rectangle

# Substitutability violated
class Square:
    def process(self):
        # Process square (intended)

# Better design respecting LSP
class Shape:
    def process(self):
        pass

class Rectangle(Shape):
    def process(self):
        # Process rectangle

class Square(Shape):
    def process(self):
        # Process square

Interface Segregation Principle (ISP)

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Violating ISP
interface Worker {
    void work();
    void eat();
}

// Better design respecting ISP
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Violating ISP
class Worker:
    def work(self):
        # Work

    def eat(self):
        # Eat

# Better design respecting ISP
class Workable:
    def work(self):
        pass

class Eatable:
    def eat(self):
        pass

Dependency Inversion Principle (DIP)

C Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Violating DIP
class Service {
    Database db;

    void process() {
        db.connect();
        // Process data
        db.disconnect();
    }
}

// Better design following DIP
interface DBConnector {
    void connect();
    void disconnect();
}

class Service {
    DBConnector db;

    void process() {
        db.connect();
        // Process data
        db.disconnect();
    }
}

Python Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Violating DIP
class Service:
    def __init__(self):
        self.db = Database()

    def process(self):
        self.db.connect()
        # Process data
        self.db.disconnect()

# Better design following DIP
class DBConnector:
    def connect(self):
        pass

    def disconnect(self):
        pass

class Service:
    def __init__(self, db):
        self.db = db

    def process(self):
        self.db.connect()
        # Process data
        self.db.disconnect()

Conclusion

In this blog post, we explored the best practices of unit testing and the application of SOLID principles with extensive examples in C and Python. By adhering to these principles and incorporating unit testing into the development process, software engineers can build robust, maintainable, and scalable codebases.

Do you have any questions or additional examples related to unit testing and SOLID principles? Feel free to share your thoughts in the comments below!


➡️ Simple event handler library


⬅️ Get directory of currently run python script


Go back to Posts.