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:
- Single Responsibility Principle (SRP): A class should have only one reason to change.
- Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification.
- Liskov Substitution Principle (LSP): Derived types must be substitutable for their base types without altering the correctness of the program.
- Interface Segregation Principle (ISP): A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
- Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions, and abstractions should not depend on details.
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!
Go back to Posts.