ASM Final APDP QuangBinhQuan BH00999
ASM Final APDP QuangBinhQuan BH00999
Unit number and title Unit 20: Applied Programming and Design Principles
Plagiarism
Plagiarism is a particular form of cheating. Plagiarism must be avoided at all costs and students who break the rules, however innocently, may be
penalised. It is your responsibility to ensure that you understand correct referencing practices. As a university level student, you are expected to use
appropriate references throughout and keep carefully detailed notes of all your sources of materials for material you have used in your work,
including any material downloaded from the Internet. Please consult the relevant unit lecturer or your course tutor if you need any further advice.
Student Declaration
I certify that the assignment submission is entirely my own work and I fully understand the consequences of plagiarism. I declare that the work
submitted for assessment has been carried out without assistance other than that which is acceptable according to the rules of the specification. I
certify I have clearly referenced any sources and any artificial intelligence (AI) tools used in the work. I understand that making a false declaration is
a form of malpractice.
P1 P2 P3 P4 M1 M2 D1
ASSIGNMENT GROUP WORK
Qualification Pearson BTEC Level 5 Higher National Diploma in Computing
Unit number and title Unit 20: Applied Programming and Design Principles
Plagiarism
Plagiarism is a particular form of cheating. Plagiarism must be avoided at all costs and students who break the rules, however innocently, may be penalised. It is your
responsibility to ensure that you understand correct referencing practices. As a university level student, you are expected to use appropriate references throughout and
keep carefully detailed notes of all your sources of materials for material you have used in your work, including any material downloaded from the Internet. Please
consult the relevant unit lecturer or your course tutor if you need any further advice.
Student Declaration
I certify that the assignment submission is entirely my own work and I fully understand the consequences of plagiarism. I declare that the work submitted for
assessment has been carried out without assistance other than that which is acceptable according to the rules of the specification. I certify I have clearly referenced any
sources and any artificial intelligence (AI) tools used in the work. I understand that making a false declaration is a form of malpractice.
P5 P6 P7 M3 M4 D2
OBSERVATION RECORD
Student
Student
Date:
signature:
Assessor
Date:
signature:
Assessor
name:
Summative Feedback: Resubmission Feedback:
The first part explores the application of good design principles, with a focus on the object-oriented
paradigm, class relationships, and SOLID principles. It also examines clean coding techniques and their
significant impact on data structures, algorithms, and system architecture, specifically within the context
of a Student Information Management System (SIMS). Furthermore, it analyzes the use of creational,
structural, and behavioral design patterns, and evaluates how the adherence to SOLID principles
influences the development process.
The second part of the report transitions into testing methodologies, discussing various types of testing,
including automation, unit testing, integration testing, and end-to-end testing. It also investigates the
effectiveness of SOLID principles and clean coding techniques in the development of applications and
evaluates the benefits and drawbacks of different forms of automated testing. Through this
comprehensive analysis, the report aims to provide a deep understanding of how design principles and
testing strategies play an integral role in the successful development and deployment of software
systems.
REPORT PART 1
Task 1. How Good Design Principles Enhance System Quality.
1. Investigate Object-Oriented Paradigm.
Object-Oriented Programming (OOP) is a programming paradigm that uses objects and classes to design
and develop software applications. It is based on the concept of objects, which can contain data in the
form of fields (attributes or properties) and code in the form of procedures (methods or functions).
Object-oriented programming contains various structures, known as the building blocks of OOP. These
structures include:
• Class: A class is a data type that provides a framework for creating objects. You can define a class
to create multiple objects without writing additional code.
• Object: In OOP, an object represents an instance, or creation, of a class. Objects define specific
data, such as properties and behaviors, to implement code.
• Method: A method is a function that performs a task or action. For example, a method may
return information about an object's data.
• Attribute: This structure stores information about an object and defines its state. You can define
an attribute as part of the class.
• Modularity: OOP promotes modularity by breaking down complex systems into smaller,
manageable parts (objects). This makes it easier to maintain and update the code.
• Reusability: OOP allows you to reuse existing code by creating new objects based on existing
ones. This saves time and effort in developing new applications.
• Flexibility: OOP provides flexibility in designing and implementing software systems. You can
easily modify and extend the functionality of objects without affecting other parts of the system.
• Scalability: OOP supports scalability by allowing you to add new objects and classes as the system
grows. This makes it easier to accommodate changes and enhancements in the software.
As you can see, OOP offers several advantages that makes it a popular choice for developing software
applications. Let's explore the four fundamental pillars of OOP in more detail.
• Inheritance: child classes inherit data and behaviors from the parent class
Inheritance in C#
Inheritance is referred to as "is a" relationship. In the real world example, a customer is a person. In the
same way, a student is a person and an employee is also a person. They all have some common things, for
example, they all have a first name, middle name, and last name. So to translate this into object-oriented
programming, we can create the Person class with first name, middle name, and last name properties and
inherit the Customer, Student, and Employee classes from the Person class. That way we don't need to
create the same properties in all classes and avoid the violation of the DRY (Do not Repeat Yourself)
principle.
Figure 2 Inheritance in C# example
In C#, use the : symbol to inherit a class from another class. For example, the following Employee class
inherits from the Person class in C#.
In the above example, the Person class is called the base class or the parent class, and the Employee class
is called the derived class or the child class.
The Employee class inherits from the Person class and so it automatically acquires all the public members
of the Person class. It means even if the Employee class does not include FirstName, LastName properties
and GetFullName() method, an object of the Employee class will have all the properties and methods of
the Person class along with its own members.
Figure 4 Example: Inherited Members
Note that C# does not allow a class to inherit multiple classes. A class can only achieve multiple inheritances
through interfaces.
Access modifiers play an important role in inheritance. Access modifiers of each member in the base class
impact their accessibility in the derived class.
PRIVATE No No
PROTECTED No No
Constructors
Creating an object of the derived class will first call the constructor of the base class and then the derived
class. If there are multiple levels of inheritance then the constructor of the first base class will be called
and then the second base class and so on.
Figure 5 Example: Constructors in Inheritance
Object Initialization
You can create an instance of the derived class and assign it to a variable of the base class or derived
class. The instance's properties and methods are depending on the type of variable it is assigned to. Here,
a type can be a class or an interface, or an abstract class.
The following table list supported members based on a variable type and instance type.
The following program demonstrates supported members based on the variable type:
Figure 6 Example: Object Creation
In the above example, the type of per2 is Person, so it will only expose public properties of the Person type
even if an object type is the Employee. However, the type of emp is Employee and so it exposes all the
public properties of both classes. Note that the base type object cannot be assigned to the derived type
variable.
Types of Inheritance
There are different types of inheritance supported in C# based on how the classes are inherited.
Single Inheritance
In a single inheritance, only one derived class inherits a single base class.
Figure 7 Single Inheritance
Multi-level Inheritance
In multi-level inheritance, a derived class inherits from a base class and then the same derived class
becomes a base class for another derived class. Practically, there are no limits on the level of inheritance,
but you should avoid it.
Hierarchical Inheritance
In hierarchical inheritance, multiple derived classes inherit from a single base class.
Figure 9 Hierarchical Inheritance
Hybrid Inheritance
Multiple Inheritance
In multiple inheritance, a class inherits from multiple interfaces. Note that C# does not support deriving
multiple base classes. Use interfaces for multiple inheritance.
Figure 11 Multiple Inheritance
Important Points:
• In C#, three types can participate in inheritance: Class, Struct, and Interface.
• A class can inherit a single class only. It cannot inherit from multiple classes.
• A Struct can inherit from one or more interfaces. However, it cannot inherit from another struct
or class.
• An interface can inherit from one or more interfaces but cannot inherit from a class or a struct.
Encapsulation
Encapsulation is a technique to implement abstraction in code. Create classes and their members with
appropriate access modifiers to show or hide details and complexity.
Encapsulation hides the data and implementation details show only the required members within a class,
thus hiding complexity from other code. No other code needs to know about implementation detail and
also can’t modify the code of the class’s data and methods.
Most object-oriented programming languages allow you to create classes and their properties and
methods along with the access modifiers such as public, private, protected, and internal to show or hide
data members and implementation details. Interfaces and abstract classes can also be used for
encapsulation.
For example, the Student class has the following members:
As you can see, the FirstName, MiddleName, LastName, and FullName are data members
and Save(), Subscribe(), GetSubscribedCourses() are methods.
In C#, we can implement encapsulation mostly using class, interface, abstract class, property, method,
struct, enum, and access modifiers. For the above Student entity, we can create the Student class. Use
properties for the data members and methods for the actions.
Above, private fields such as _firstName, _middleName, and _lastName store the data privately to hide it
from the external code so that they cannot modify it with invalid values. FirstName, MiddleName,
and LastName properties use these fields in the getters and setters to return and set values to these fields.
These are public properties so that they are visible and accessible to outside code via getters and setters.
The FullName property internally uses private variables to return the full name of the student.
In the same way, the public Enroll() method is visible but it hides the implementation detail by internally
calling the private Subscribe() method. External code cannot know and access the Subscribe() method
because it is a private method.
Advantages of Encapsulation:
• Only the author of the class needs to understand the implementation, not others.
Abstraction is a key concept in object-oriented programming that allows you to create a blueprint for a
class with some abstract methods that must be implemented by the derived classes. It enables you to
define the structure of a class without providing the implementation details.
In C#, Abstraction can be achieved using abstract classes and interfaces. Let's explore both concepts:
Abstract Classes
An abstract class is a class that cannot be instantiated and can contain both abstract and non-abstract
methods. An abstract method is a method without a body that must be implemented by the derived
classes.
In this example, the Animal class is an abstract class with an abstract method Speak.The Dog and Cat classes
inherit from the Animal class and provide specific implementations for the Speak method. This is an
example of Abstraction using abstract classes in C#.
Interfaces
An interface is a reference type in C# that defines a contract for classes to implement. It contains only the
declaration of the methods, properties, events, or indexers, without providing the implementation.
In this example, the IFlyable interface defines a contract with a method Fly. The Bird and Airplane classes
implement the IFlyable interface and provide specific implementations for the Fly method. This is an
example of Abstraction using interfaces in C#.
Polymorphism
Polymorphism is a core concept in object-oriented programming that allows objects of different classes to
be treated as objects of a common superclass. It provides a single interface to represent multiple
underlying forms (classes) and enables objects to be processed in a generic manner.
Compile-time Polymorphism, also known as Method Overloading, allows a class to have multiple methods
with the same name but different parameters. The compiler determines which method to invoke based on
the number and types of arguments.
In this example, the Printer class has three Print methods with the same name but different parameters.
This is an example of Method Overloading in C#.
Run-time Polymorphism, also known as Method Overriding, allows a subclass to provide a specific
implementation of a method that is already provided by its superclass.
In this example, the MusicPlayer class has a virtual method Play. The Mp3Player and WavPlayer classes
override the Play method with specific implementations for playing MP3 and WAV music, respectively.
This is an example of Method Overriding in C#.
In this code snippet, we created an object of the Mp3Player class and assigned it to a variable of
type MusicPlayer. We then called the Play method on the player object, which invokes the
overridden Play method in the Mp3Player class. We then created an object of the WavPlayer class and
assigned it to the player variable. When we call the Play method again, it invokes the
overridden Play method in the WavPlayer class.
2. Class Relationships.
There are six main types of relationships between classes: inheritance , realization / implementation ,
composition , aggregation , association, and dependency . The arrows for the six relationships are as
follows:
In the six types of relationships, the code structure of the three types of relationships such as composition,
aggregation , and association is the same as using attributes to store the references of another class.
Therefore, they must be distinguished by the relationship between the contents.
Inheritance
Inheritance is also called generalization and is used to describe the relationship between parent and
child classes. A parent class is also called a base class, and a subclass is also called a derived class.
In the inheritance relationship, the subclass inherits all the functions of the parent class, and the parent
class has all the attributes, methods, and subclasses. Subclasses contain additional information in
addition to the same information as the parent class.
For example: buses, taxis, and cars are cars, they all have names, and they can all be on the road.
Figure 20 Example: Inheritance
Realization / Implementation
Implementation (Implementation) is mainly used to specify the relationship between interfaces and
implementation classes .
For example: cars and ships are vehicles, and the vehicle is just an abstract concept of a mobile tool, and
the ship and the vehicle realize the specific mobile functions.
Composition relationship
Composition: The relationship between the whole and the part, but the whole and the part cannot be
separated .
The combination relationship represents the relationship between the whole and part of the class, and
the overall and part have a consistent lifetime. Once the overall object does not exist, some of the
objects will not exist, and they will all die in the same life.For example, a person is composed of a head
and a body. The two are inseparable and coexist.
Aggregation Relationship
Aggregation: The relationship between the whole and part, and the whole and part can be separated.
Aggregate relations also represent the relationship between the whole and part of the class, member
objects are part of the overall object, but the member object can exist independently from the overall
object.
For example, bus drivers and work clothes and hats are part of the overall relationship, but they can be
separated. Work clothes and hats can be worn on other drivers. Bus drivers can also wear other work
clothes and hats.
Association Relationships
Association: Indicates that a property of a class holds a reference to an instance (or instances) of
another class .
Association is the most commonly used relationship between a class and a class, which means that there
is a connection between one type of object and another type of object. Combinations and aggregations
also belong to associative relations , but relations between classes of affiliations are weaker than the
other two.
There are four kinds of associations : two-way associations , one-way associations , self-association ,
and multiple-number associations .
For example: cars and drivers, one car corresponds to a particular driver, and one driver can drive
multiple cars.
In UML diagrams, bidirectional associations can have two arrows or no arrows , and one-way
associations or self-associations have an arrow .
In a multiplicity relationship, you can add a number directly to the associated line to indicate the number
of objects in the corresponding class.
• 1..*:one or more
Dependencies
Dependence: Assume that a change in class A causes a change in class B, then say that class B depends on
class A.
In most cases, dependencies are reflected in methods of a class that use another class’s object as a
parameter .
A dependency relationship is a “use” relationship. A change in a particular thing may affect other things
that use it, and use a dependency when it is necessary to indicate that one thing uses another.For
example: The car relies on gasoline. If there is no gasoline, the car will not be able to drive.
Among the six types of relationships, the code structure of combination, aggregation, and association is
the same, and it can be understood from the strength of the relationship. The order from strong to weak
is: inheritance → implementation → composition → aggregation → association → dependency . The
following is a complete UML diagram.
3. SOLID Principles.
Why should we use the SOLID design principles?
SOLID principles serve as foundational guidelines in software development, aiming to create robust and
adaptable systems. By adhering to SOLID principles, developers foster:
• Future scalability: Allows for more adaptable and extensible software systems. Developers should
design code that can be added to but not modified, allowing others to introduce new features or
functionalities without altering existing code. This scalability is essential for accommodating
future changes and expanding the system’s capabilities without compromising stability.
Incorporating the SOLID design principles in software development leads to a more robust, adaptable,
and maintainable codebase. Let’s discuss each principle with code examples.
The Single Responsibility Principle (SRP) promotes clean, maintainable, and scalable software design. It
states that a class should change for only one reason, meaning it should have a single responsibility.
Let’s consider a user creation process that involves validating and saving user data to a database.
Refer to the following code example to understand how SRP can be violated through combined handling
validation and persistence.
Issue
In the previous code, the UserCreator class violates the SRP by combining multiple responsibilities, such
as validation and database persistence. This can lead to a tightly coupled class, making it difficult to test
and prone to unnecessary modifications.
Solution
To address this issue, we can apply SRP by refactoring the code to separate these responsibilities into
individual classes.
After refactoring, the code demonstrates the implementation of SRP through the separation of
responsibilities into three classes:
• UserCreator: Coordinates the user creation process, leveraging the validator and repository
classes for their specific responsibilities.
Benefits
By separating the concerns, we achieve a more maintainable and testable codebase. Each class has a
single responsibility, allowing for more straightforward modification and extension in the future.
The Open-Closed Principle (OCP) says that software entities should be open for extension but closed for
modification. It allows for adding new functionality without modifying existing code.
Let’s consider a scenario where a file-exporting service initially supports exporting data to CSV files.
Refer to the following code example to understand how OCP can be violated and how to correct it using
C#.
Issue
In this example, the FileExporter class directly implements the functionality for exporting data to CSV
files. However, if we later want to support exporting data to other file formats like Excel or JSON,
modifying the FileExporter class would violate the OCP.
Solution
To use the OCP, we must design our file-exporting service domain to be open for extension.
In the improved implementation, we introduce an abstract FileExporter class that defines the common
behavior for all file export operations. Each specific file exporter (CsvFileExporter, ExcelFileExporter,
and JsonFileExporter) inherits from the FileExporter class and implements the Export method according
to the particular file format export logic.
Applying the OCP allows for adding new file exporters without modifying old ones, making it easier to
add new features by introducing subclasses of the FileExporter base class.
Benefits
This approach enhances code flexibility, reusability, and maintainability. Your code can seamlessly handle
new requirements and changes without introducing bugs or disrupting the existing functionality.
The Liskov Substitution Principle (LSP) is a concept that guarantees the smooth substitution of objects of
derived classes for objects of their base classes. Its fundamental rule asserts that objects of the base class
must be interchangeable with objects of any of its derived classes, without impacting the accuracy of the
program.
Refer to the following code example to understand how LSP can be violated and how to correct it using
C#.
Figure 31 Example: LSP 1
Issue
In this example, we have a Vehicle class that represents a generic vehicle. It has abstract
methods, StartEngine() and StopEngine(), for starting and stopping the engine. We also have a Car class
that inherits from Vehicle and provides the necessary implementation for the engine-related methods.
However, when we introduce a new type of vehicle, such as an ElectricCar, which doesn’t have an
engine, we encounter a violation of the LSP. In this case, attempting to call
the StartEngine() or StopEngine() methods on an ElectricCar object would result in exceptions because
electric cars do not have engines.
Figure 32 Example: LSP 2
Solution
To address this violation, we need to ensure the correct substitution of objects. One approach is to
introduce an interface called IEnginePowered that represents vehicles with engines.
In this corrected design, the Car class implements the IEnginePowered interface along with
the Vehicle class. The Vehicle class will include common vehicle properties and behavior for both. This
design provides the necessary implementation for the engine-related methods. Also, the ElectricCar class
does not implement the IEnginePowered interface because it does not have an engine.
We can substitute objects of the Car or ElectricCar class where instances of the IEnginePowered are
expected. The ElectricCar class does not need to implement engine-related methods.
Benefits
Using the LSP, we ensured that the program remained accurate and consistent when substituting objects
of derived classes for objects of their base class.
The Interface Segregation Principle (ISP) says to create smaller, specialized interfaces that cater to clients’
specific needs. It discourages large interfaces that include unnecessary methods, so that clients are not
burdened with functionality they don’t require.
Refer to the following example to understand how ISP can be violated and how to correct it using C#.
Figure 35 Example: ISP 1
Issue
In the previous example, we have an IOrder interface that contains methods for placing an order,
canceling an order, updating an order, calculating the total, generating an invoice, sending a confirmation
email, and printing a label.
However, not all client classes implementing this interface require or use all these methods. This violates
ISP, since clients are forced to depend on methods they don’t need.
By following the ISP, we can refactor the code by segregating the interface into smaller, more focused
interfaces.
Figure 36 Example: ISP 2
Solution
By segregating the interfaces, we now have smaller, more focused interfaces that clients can choose to
implement based on their specific needs. This approach eliminates unnecessary dependencies and allows
for better extensibility and maintainability. Clients can implement only the interfaces they require,
resulting in cleaner code that is easier to understand, test, and modify.
Benefits
Using the ISP in C# enables us to create interfaces tailored to specific client requirements. By avoiding the
violation of ISP, we can build more flexible, modular, and maintainable code. Breaking down large
interfaces into smaller, cohesive ones reduces coupling and improves code organization.
The Dependency Inversion Principle (DIP) focuses on decoupling high-level modules from low-level
modules by introducing an abstraction layer, with the use of interfaces or abstract classes and reducing
direct dependencies between classes.
Refer to the following example where a UserController class depends directly on a Database class for
data storage.
Issue
In the previous example, the UserController tightly couples with the concrete Database class, creating a
direct dependency. If we decide to alter the database implementation or introduce a new storage
mechanism, we will need to modify the UserController class, which violates the Open-Closed Principle.
Solution
To address this issue and adhere to the DIP, we must invert the dependencies by introducing an
abstraction that both high-level and low-level modules depend on. Typically, this abstraction is defined
using an interface or an abstract class.
In this updated version, we introduce the IDataStorage interface that defines the contract for data
storage operations. The Database class implements this interface, providing a concrete implementation.
Consequently, the UserController class now relies on the IDataStorage interface rather than the
concrete Database class, resulting in it being decoupled from specific storage mechanisms.
This inversion of dependencies facilitates easier extensibility and maintenance. We can introduce new
storage implementations, such as a file system or cloud storage, by simply creating new classes that
implement the IDataStorage interface, without modifying the UserController or any other high-level
modules.
Benefits
By applying the DIP, we achieve a more flexible and modular design, enabling us to evolve and adapt our
systems more easily over time.
SOLID principles offer a set of guidelines that significantly impact the quality, maintainability, and
scalability of software systems. Embracing these principles brings several key advantages to software
development:
• Enhanced maintainability: Following SOLID principles results in code that is easier to understand,
update, and maintain. The principles encourage clear code organization, making it simpler to
identify and modify specific functionalities without impacting the rest of the system. This
ultimately reduces the chances of introducing bugs during maintenance.
• Improved extensibility: By adhering to SOLID principles, code becomes more flexible and open
for extension without requiring modifications to existing, functional code. This extensibility is
particularly beneficial when adding new features or accommodating changing requirements, as it
minimizes the risk of unintentionally breaking existing functionalities.
• Facilitates reusability: SOLID principles promote the creation of modular, loosely coupled
components. This modularity allows for greater code reuse across different parts of the system or
in entirely new projects, leading to more efficient development processes.
• Supports testability: Codes that follow SOLID principles tend to be more testable. The principles
advocate for smaller, more focused units of code with clear responsibilities, making it easier to
write unit tests and ensure proper functionality.
• Reduces technical debt: Implementing SOLID principles from the start reduces the accumulation
of technical debt. It encourages clean code practices, preventing issues such as code smells, tight
coupling, and unnecessary complexity. Consequently, it saves time and effort that might
otherwise be spent refactoring or fixing problematic code later in the development lifecycle.
Task 2. Clean Coding Techniques and Their Impact on Data
Structures and Algorithms.
1. Clean Coding Techniques.
Tips for Writing Clean Code
Writing clean and high-quality code is crucial to maintaining the readability, efficiency, and scalability of
our applications in C# .NET. By following the principles of Clean Code, we can improve the understanding
and maintainability of our code. In this article, we will share 10 practical tips for writing clean code in C#
.NET. From meaningful names to applying SOLID principles, these tips will help you elevate the quality of
your code and foster a more professional software development culture.
Using descriptive and meaningful names is essential for clean code. Avoid confusing abbreviations and
choose names that clearly reflect the intention of the variable or function. Here's an example:
Long and complex functions can make code reading and maintenance challenging. It is preferable to
break down functions into smaller and self-descriptive units. This improves readability and facilitates
code reuse. Here's an example:
3. Avoid code duplication
Code duplication can increase the likelihood of errors and make maintenance difficult. If you find similar
code blocks, it is recommended to extract them into separate methods or functions to promote reuse.
Here's an example:
4. Keep functions and methods small
Using small functions and methods improves code readability and understanding. Here's an example:
5. Apply the DRY principle
Avoid code duplication by reusing functions or creating proper abstractions. Here's an example:
2. Impact on Data Structures and Operations.
C# Queue
A linear data structure that follows the First In First Out (FIFO) principle. Elements are added at the end
(enqueue) and removed from the front (dequeue).
Example:
Output
Here, fruits is a queue that contains string elements ("Apple" and "Orange").
C# Queue Methods
• Dequeue() - removes and returns an element from the beginning of the queue
• Peek() - returns an element from the beginning of the queue without removing
To add an element to the end of the queue, we use the Enqueue() method. For example,
Output
In the above example, we have created Queue class named numbers.
Since the queue follows FIFO principle, the element added at the first (65) is displayed at the first in the
output.
To remove an element from the beginning of the queue, we use the Dequeue() method. For example,
Output
In the above example, we have used the Dequeue() method to remove an element from
the colors queue.
The method removed and returned "Red" from the beginning of the queue.
The Peek() method returns the element from the beginning of the queue without removing it. For
example,
Output
Here, we have displayed the element present at the beginning of the planet queue using
the Peek() method.
We can use the Contains() method to check whether an element is present inside the queue or not.
The method returns True if a specified element exists in the queue. If not it returns False. For example,
Output
C# ArrayList: A resizable array implementation that allows for dynamic addition and removal of elements.
Provides indexed access to elements.
• Add Elements
• Access Elements
• Change Elements
• Remove Elements
C# provides a method Add() using which we can add elements in ArrayList. For example,
In the above example, we have created an ArrayList named student.
Then we added "Tina" and 5 to the ArrayList using the Add() method.
We use indexes to access elements in ArrayList. The indexing starts from 0. For example,
Output
Iterate ArrayList
In C#, we can also loop through each element of ArrayList using a for loop. For example,
Output
In the above example, we have looped through myList using a for loop.
Output
C# provides methods like Remove(), RemoveAt(), RemoveRange() to remove elements from ArrayList.
In the above example, we have removed "Jack" from myList using the Remove() method.
Task 3. Designing the Architecture and Testing Regime for SIMS.
1. Use case diagram
Explaination
Actors:
1. Admin
• Add Faculty: Admin can add new faculty members to the system.
• Update Faculty: Admin can update details of existing faculty members.
• Remove Faculty: Admin can remove faculty members from the system.
• Add Student: Admin can add new students to the system.
• Update Student: Admin can update details of existing students.
• Remove Student: Admin can remove students from the system.
• Add Course: Admin can add new courses to the system.
• Update Course: Admin can update details of existing courses.
• Remove Course: Admin can remove courses from the system.
2. Student:
• Enter Grades: Faculty can enter grades for students enrolled in their courses.
• Manage Courses: Faculty can manage courses they are responsible for, including updating
course details.
2. Class diagram
Explanation
• Student Registration:
o Student class properties (studentID, courses, inherited name, email, etc.) ensure capturing
and storing essential information.
• Course Management:
o Admin class methods (addCourse, removeCourse, updateCourse) enable course
management.
o Inheritance from User (Student, Faculty, Admin) implements role-based access control by
segregating functionalities based on user roles.
3. Package diagram
Explanation
Packages:
1. Controllers:
o These classes handle HTTP requests, interact with services, and return appropriate HTTP
responses.
2. Services:
o Service classes contain business logic and interact with repositories to retrieve or update
data.
3. Repositories:
o Repository classes handle data access operations and interact with the data source (e.g., a
CSV file).
4. Models:
• Definition: A class should have only one reason to change, meaning it should have only one job or
responsibility.
• Application:
o Faculty: Manages faculty-specific properties and behaviors like managing courses and
entering grades.
o Admin: Manages administrative tasks like adding, removing, and updating students,
courses, and faculty.
o Course: Manages course-specific information and enrolled students.
Each class has a single responsibility, ensuring they are easier to understand, maintain, and extend.
• Definition: Software entities should be open for extension but closed for modification.
• Application:
o Inheritance: The User class is extended by Student, Faculty, and Admin. New
functionalities can be added to these subclasses without modifying the User class.
• Definition: Subtypes must be substitutable for their base types without altering the correctness of
the program.
• Application:
o Inheritance: Student, Faculty, and Admin can be used wherever User is expected. This
ensures that derived classes can be substituted for base classes without affecting the
application’s functionality.
• Definition: Clients should not be forced to depend on interfaces they do not use.
• Application:
• Definition: High-level modules should not depend on low-level modules; both should depend on
abstractions.
• Application:
Integration Testing
Purpose: Creational patterns deal with object creation mechanisms, trying to create objects in a manner
suitable to the situation. They help in managing object creation by abstracting the instantiation process.
Pattern Examples:
• Singleton Pattern: Ensures that a class has only one instance and provides a global point of access
to it.
Example:
In the context of SIMS, a UserRepository class might use the Singleton pattern if you want to ensure that
only one instance of the repository is used throughout the application. This can be beneficial to avoid
duplicate data sources or conflicting state.
Code Example:
Purpose: Structural patterns deal with object composition or the structure of classes. They help in
creating a structure where classes and objects can work together.
Pattern Examples:
• Adapter Pattern: Allows incompatible interfaces to work together. It acts as a bridge between
two incompatible interfaces.
Example:
If the SIMS application needs to interact with an external system or API that provides user data in a
different format, an Adapter pattern could be used to translate between the external data format and
the internal data structures.
Code Example:
Purpose: Behavioral patterns focus on communication between objects, what goes on between objects
and how responsibilities are distributed.
Pattern Examples:
• Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them
interchangeable. It lets the algorithm vary independently from clients that use it.
Example:
In SIMS, to provide more different ways to calculate student grades (e.g., by average, by highest grade),
we could use the Strategy pattern to encapsulate these different strategies.
Code Example:
Output:
By applying these patterns, the SIMS application achieves better modularity, flexibility, and
maintainability. Each pattern addresses specific design challenges and helps in creating a well-structured
and scalable system.
Why Applied: To ensure that each class has only one reason to change, making the system easier to
maintain and understand.
Example in Code:
Code Example:
Application: Classes are open for extension but closed for modification.
Why Applied: To allow the system to be extended with new functionality without modifying existing
code, thus reducing the risk of introducing bugs.
Effectiveness and Benefits:
• Reduced risk: Existing code remains untouched, minimizing the chance of introducing errors.
Example in Code:
• UserRepository.cs: Can be extended to use different data sources without changing its existing
code.
Code Example:
Why Applied: To ensure that a derived class can be used interchangeably with its base class without
altering the correctness of the program.
Effectiveness and Benefits:
• Ensures consistency: Subtypes behave as expected when used in place of base types.
Example in Code:
• Student.cs: Inherits from User and can be used wherever User is expected.
Code Example:
Application: Clients should not be forced to depend on interfaces they do not use.
Why Applied: To prevent clients from being forced to implement methods they do not use, ensuring a
more decoupled and cohesive design.
• Improved decoupling: Reduces the impact of changes, making the system easier to maintain.
Example in Code:
Code Example:
Application: High-level modules should not depend on low-level modules but on abstractions.
Why Applied: To reduce the coupling between high-level and low-level modules, making the system
more flexible and easier to change.
• Improved maintainability: High-level logic remains unchanged when low-level details are
modified.
Example in Code:
Code Example:
Note that the concept of Automation Testing is not strictly limited to the Execution phase, although it is
generally understood and accepted that automation testing means “automatically executing a test case”.
Automation testing can actually be an umbrella term referring to the “automation of any testing activity
across the testing life cycle”.
Automation testing is the best way to enhance effectiveness, broaden test coverage, and improve
execution speed in software testing. There are several reasons why Automation Testing is crucial:
• Improved Accuracy: Automation testing reduces the likelihood of human errors as automated
tests are designed to strictly follow a set of predefined steps. Automated tests eliminate the
chances of human testers introducing errors like forgetting a certain step while executing the
tests.
• Increased Speed: Automated tests can run continuously, in parallel, 24/7, without the need for
human intervention, further increasing the speed of test execution and reducing the overall
testing time.
• Consistency: A lack of testing consistency and standardization can result in missing important
issues. In continuous testing scenarios, such as triggering regular regression test runs, automation
suites are indispensable. With automation testing, test cases are executed in exactly the same
way every time they are run. They can also be run multiple times a day, ensuring that new issues
are quickly identified and resolved, leading to improved confidence in the software quality.
• Cost Savings: Projects dealing with more advanced infrastructures or a variety of application
types see the highest automation ROI. The costs of time, technologies and human resources are
often the biggest blockers to automation adoption. Not only pertaining to software test
automation, setting up and standardizing automated workflows don’t happen overnight.
However, the long-term ROI in accuracy, speed, and consistency is guaranteed.
• Enhanced Test Coverage: Automated test suites can be reused to run against multiple browsers,
devices and operating systems combinations. Instead of performing the exact test steps over and
over again, simply lock down the most common environment your users are accessing and press
run. Using cloud environments is also an effective practice to test on older versions of browsers,
devices and operating systems (e.g., iOS 13).
• Improved Test Reusability: Once automated tests are created, they can be stored and reused
across multiple systems with the click of a button, and testers do not need to spend time re-
creating and executing tests for each testing cycle.
• Continuous Testing: Automated tests can be run frequently and at any stage of the development
process, whether it's during the development phase, integration phase, or after deployment.
They can even be integrated into the development pipeline, so that they are run automatically
every time new changes are made to the software.
If you're a software business or an individual working in the digital industry, investing in automation
testing will surely bring immense benefits to your product development process.
The decision to automate a test case should be based on a careful consideration of the potential benefits
and costs of its automation. Following test cases are ideal candidates for test automation:
It is also important to note that not all test cases are suitable for automation. Test cases for which the
requirements are frequently changing and test cases executed on an ad-hoc basis should not be
automated due to their unpredictable nature.
• Industries: automated testing is widely used in many industries, most commonly being IT,
eCommerce, Banking & Finance, Insurance, Telecommunications, Gaming, and even Education.
Any industry with business models revolving around high functionality, stability, digital presence,
or top-notch user experience can benefit tremendously by adopting automation testing.
• Application Under Tests: Automation testing can be used to test various quality aspects of
websites, mobile applications, desktop applications, and API. For example, tests to verify the
functionality, performance, security and usability of a website can easily be automated. Mobile
applications can be tested for compatibility with different operating systems, devices, and screen
sizes.
• Testing types: Testers can automate a wide range of testing types, including regression testing,
acceptance testing, unit testing, or integration testing, to name a few. Here are 15 types of QA
testing you should know.
• Testing environment: Automation testing can be used across different operating systems,
browsers, and devices.
Similar to any other testing type, you first need to decide on the approach for your automation testing
project. There are usually 2 main approaches:
In other words:
• To best utilize a framework, you must have extensive coding knowledge yourself, and the effort
to maintain test scripts is significant, but you can freely customize the framework to fit your
specific testing needs
• A testing tool doesn’t require coding so you can easily create tests much faster. You can switch to
coding mode whenever you want (if the tool offers coding mode). Test maintenance is usually
taken care of, which reduces the workload. However, you are bound by the features offered by
the tool, so it is recommended to list out the features you want from a tool to select the one that
best fits your demands.
Of course, automation testing is not without its challenges. There is usually some compromise to be
made when you want to increase testing speed. What’s important is whether that compromise is worth it
or not.
According to the State of Quality Report 2024, the most prevalent challenge to automation testing is the
team's lack of skills and experience in test automation, with up to 45% of respondents agreeing, followed
by 38% thinking that requirements change too often, and another 26% claiming that test maintenance is
costly.
Figure 69 Challenges of Automation Testing
The tool should be able to support the application being tested and the testing requirements. Selecting
an inappropriate tool can result in test automation failure as well as inefficient use of testing budget.
Some criteria for you to consider:
• Compatibility: this automation testing tool should be compatible with your software
development environment, including your operating system, programming language, and any
other tools you are using.
• Functionality: the tool should have the necessary functionalities to create, run, report and debug
tests. Additionally, assess whether the tool’s strength (e.g., web UI testing) matches with your
testing needs most.
• Scalability: it should be scalable to meet the demands of your testing needs, both now and in the
future, as your software evolves and grows.
• Integration: it should also be able to integrate with other tools you are using, such as your bug
tracking system or continuous integration platform, to help streamline your testing process.
• Support: there should be good customer support and a vibrant community, with resources such
as forums, online tutorials, and knowledge bases
• Security: the tool should have adequate security measures in place to protect your data and
ensure that your tests are performed securely.
A good choice is adopting software quality management platforms that integrate multiple testing
functionalities (web, API, mobile) into a unified solution.
These platforms offer end-to-end capabilities from test planning to reporting, including features like
requirements mapping and automation script management. By consolidating various testing tools into
one platform, they streamline testing workflows and eliminate the need for managing multiple tools
separately.
A unit test is a block of code that verifies the accuracy of a smaller, isolated block of application code,
typically a function or method. The unit test is designed to check that the block of code runs as expected,
according to the developer’s theoretical logic behind it. The unit test is only capable of interacting with
the block of code via inputs and captured asserted (true or false) output.
A single block of code may also have a set of unit tests, known as test cases. A complete set of test cases
cover the full expected behavior of the code block, but it’s not always necessary to define the full set of
test cases.
When a block of code requires other parts of the system to run, you can’t use a unit test with that
external data. The unit test needs to run in isolation. Other system data, such as databases, objects, or
network communication, might be required for the code’s functionality. If that's the case, you should use
data stubs instead. It’s easiest to write unit tests for small and logically simple blocks of code.
To create unit tests, you can follow some basic techniques to ensure coverage of all test cases.
Logic checks
Does the system perform the right calculations and follow the right path through the code given a
correct, expected input? Are all paths through the code covered by the given inputs?
Boundary checks
For the given inputs, how does the system respond? How does it respond to typical inputs, edge cases, or
invalid inputs?
Let’s say you expect an integer input between 3 and 7. How does the system respond when you use a 5
(typical input), a 3 (edge case), or a 9 (invalid input)?
Error handling
When there are errors in inputs, how does the system respond? Is the user prompted for another input?
Does the software crash?
Object-oriented checks
If the state of any persistent objects is changed by running the code, is the object updated correctly?
If there are any input, output, or logic-based errors within a code block, your unit tests help you catch
them before the bugs reach production. When code changes, you run the same set of unit tests—
alongside other tests such as integration tests—and expect the same results. If tests fail (also
called broken tests) it indicates regression-based bugs.
Unit testing also helps finds bugs faster in code. Your developers don’t spend a large amount of time on
debugging activities. They can quickly pinpoint the exact part of the code that has an error.
Documentation
It's important to document code to know exactly what that code is supposed to be doing. That said, unit
tests also act as a form of documentation.
Other developers read the tests to see what behaviors the code is expected to exhibit when it runs. They
use the information to modify or refactor the code. Refactoring code makes it more performant and well-
composed. You can run the unit testing again to check that code works as expected after changes.
Developers use unit tests at various stages of the software development lifecycle.
Test-driven development
Test-driven development (TDD) is when developers build tests to check the functional requirements of a
piece of software before they build the full code itself. By writing the tests first, the code is instantly
verifiable against the requirements once the coding is done and the tests are run.
Once a block of code is considered complete, unit tests should be developed if they have not been
already thanks to TDD. Then, you can immediately run unit tests to verify the results. Unit tests are also
run as part of the full suite of other software tests during system testing. They're typically the first set of
tests that run during full system software testing.
DevOps efficiency
One of the core activities in the application of DevOps to software development practices is continuous
integration and continuous delivery (CI/CD). Any changes to the code are automatically integrated into
the wider codebase, run through automated testing, and then deployed if the tests pass.
Unit tests make up part of the test suite alongside integration testing. They run automatically in the
CI/CD pipeline to ensure code quality as it is upgraded and changed over time.
Once your developers start writing tests, they also see refactoring opportunities in the block of code and
get distracted from completing them. This can lead to extended development timelines and budget
issues.
UI/UX applications
When the main system is concerned with look and feel rather than logic, there may not be many unit
tests to run. Other types of testing, such as manual testing, are a better strategy than unit testing in
these cases.
Legacy codebases
Writing tests to wrap around existing legacy code can prove to be near impossible, depending on the
style of the written code. Because unit tests require dummy data, it can also be too time-intensive to
write unit tests for highly interconnected systems with a lot of data parsing.
Depending on the project, the software can grow, change directions, or have whole parts scrapped
altogether in any given work sprint. If requirements are likely to change often, there's not much reason
to write unit tests each time a block of code is developed.
We give some unit testing best practices to get the most out of your process.
It wastes time to write explicit, fully customized unit tests for every single block of code. There are
automated testing frameworks for every popular programming language.
For instance, Python has pytest and unittest as two different frameworks for unit testing. Testing
frameworks are used extensively throughout software development projects of all sizes.
Unit testing should be triggered on different events within software development. For example, you can
use them before you push changes to a branch using version control software or before you deploy a
software update.
Unit testing may also run on a complete project, set on a timed schedule. Automated unit testing ensures
tests run in all appropriate events and cases throughout the development lifecycle.
Assert once
For each unit test, there should only be one true or false outcome. Make sure that there is only one
assert statement within your test. A failed assert statement in a block of multiple ones can cause
confusion on which one produced the issue.
Unit testing is an important part of building software, but many projects don’t dedicate resources to it.
When projects start as prototypes, are small community-based efforts, or are simply coded quickly, unit
testing can be left out due to time constraints.
However, when you build projects with unit testing as a standard practice from the beginning, the
process becomes far easier to follow and repeat.
Software is often built from many individual software components or modules. Issues between those
modules can always happen for many reasons:
• Inconsistent code logic: They are coded by different programmers whose logic and approach to
development differ from each other, so when integrated, the modules cause functional or
usability issues. Integration testing ensures that the code behind these components is aligned,
resulting in a working application.
• Shifting requirements: Clients change their requirements frequently. Modifying the code of 1
module to adapt to new requirements sometimes means changing its code logic entirely, which
affects the entire application. These changes are not always reflected in unit testing, hence the
need for integration testing to uncover the missing defects.
• Erroneous Data: Data can change when transferred across modules. If not properly formatted
when transferring, the data can’t be read and processed, resulting in bugs. Integration testing is
required to pinpoint where the issue lies for troubleshooting.
• Third-party services and API integrations: Since data can change when transferred, API and third-
party services may receive false input and generate false responses. Integration testing ensures
that these integrations can communicate well with each other.
• Inadequate exception handling: Developers usually account for exceptions in their code, but
sometimes they can’t fully see all of the exception scenarios until the modules are pieced
together. Integration testing allows them to recognize those missing exception scenarios and
make revisions.
• External Hardware Interfaces: Bugs can also arise when there is software-hardware
incompatibility, which can easily be found with proper integration testing.
There are several strategies to perform integration testing, each of which has its own advantages and
disadvantages, with the 2 most common approaches being:
• Incremental Approach
• Bottom-up approach
• Top-down approach
• Sandwich approach
Big Bang Integration testing is an integration testing approach in which all modules are integrated and
tested at once, as a singular entity. It is essentially “testing in a Big Bang fashion”.
The Big Bang integration testing process is not carried out until all components have been successfully
unit tested.
Advantages:
• Suitable for simple and small-sized systems with low level of dependency among software
components
• Management and coordination efforts are minimized since there is only one major testing phase
Disadvantages:
• Costly and time-consuming for large systems with a huge number of units as testers have to wait
until all modules have been developed to start testing
• Waiting for all modules to be developed before testing also means late defect detection
• Clearly define the interactions between each unit/function before testing to minimize missing
defects
Incremental integration testing is an approach in which 2 or more modules with closely related logic and
functionality are grouped and tested first, then gradually move on to other groups of modules, instead of
testing everything at once. The process ends when all modules have been integrated and tested.
Incremental integration testing is more strategic than Big Bang testing. It requires substantial planning
beforehand.
Advantages:
• Earlier defect detection compared to Big Bang testing since the modules are integrated and
tested as soon as they are developed. QA teams don't have to wait until all modules are available
to begin testing.
• Easier fault localization since the modules are tested in relatively small groups.
• The strategic nature of incremental integration testing can be leveraged in project management.
For example, QA managers can choose which module to test first based on urgency, priority, or
resource availability.
• The risk of encountering catastrophic failures is also significantly reduced since issues are
addressed early on from the root.
Disadvantages:
• In earlier stages of the project, certain system functionalities may not yet be available, leading to
a dependence on stubs and drivers (which are essentially mock components that will be used as
substitutes for actual components).
• The total number of tests to perform can be huge depending on the scale of the project, requiring
significant organizational resources
• Coordinating a large integration testing project with this approach may be complex
• Require a complete definition and logic of the system before it can be broken down into small
units
• The lack of system functionalities in earlier stages, if not carefully documented, may even lead to
system “blindspots” later down the road
Incremental integration testing can be further divided into 3 smaller approaches, each also comes with
its own advantages and disadvantages that QA teams need to carefully consider for their projects. These
approaches are named based on the level of impact of the software components being integrated have
on the overall system, including:
• Bottom-up approach: perform testing for low-level components first, then gradually move to
higher-level components.
• Top-down approach: perform testing for high-level components first, then gradually move to
lower-level components.
End-to-end testing (E2E testing) checks an entire software application from beginning to end, mimicking
real user interactions and data. Its goal is to find bugs that appear when all parts of the system work
together, ensuring the application performs as expected in real-world scenarios.
End-to-end testing is also referred to as E2E testing. Thanks to end-to-end testing, testers gain insights
into how the application functions from the end user’s perspective, giving them a more comprehensive
understanding the software quality before release.
End-to-end testing is essential as modern software has grown to be intricate with dozens of systems
interacting with each other simultaneously.
Even if these components function perfectly fine individually, they may still fail when integrated due to
the miscommunication between components. End to end testing is there to verify the flow of
information through the AUT, including all possible paths and dependencies. If there is an issue in any
“touchpoint” between software components and/or subsystems, testers can easily locate the root cause
and troubleshoot immediately.
Testers would want to achieve the highest level of coverage with E2E testing. All subsystems or
components of the application, such as the user interface, application server, database, as well as any
external systems that the application may interact with, should undergo E2E testing.
Advantages
1. Comprehensive Coverage:
o E2E testing covers the entire application workflow, ensuring that all components and their
interactions are working correctly. This helps catch integration issues that unit or
integration tests might miss.
2. Realistic Scenarios:
o E2E tests simulate real user scenarios, which helps identify issues that actual users might
face. This increases the likelihood of catching bugs that would affect user experience.
o By testing the entire system, E2E tests provide high confidence that the application will
function correctly in a production environment. This includes interactions with databases,
network, and other external services.
o Automating E2E tests can reduce the need for extensive manual testing, saving time and
effort. It also allows for more consistent and repeatable testing.
o E2E testing can identify issues early in the development cycle, especially those related to
system integration and data flow. This helps in addressing problems before they escalate.
Disadvantages
o E2E tests are typically more complex and time-consuming to write and maintain compared
to unit or integration tests. Changes in the application's UI or workflow can break E2E
tests, requiring frequent updates.
2. Slower Execution:
o E2E tests are slower to run because they cover the entire application and its
dependencies. This can slow down the development and continuous
integration/continuous deployment (CI/CD) processes.
o Setting up the test environment for E2E testing can be challenging. It often requires
replicating the production environment, including databases, network configurations, and
external services.
4. Debugging Difficulties:
o When an E2E test fails, it can be difficult to pinpoint the exact cause of the failure. The
issue could be anywhere in the end-to-end workflow, making debugging time-consuming.
5. False Positives/Negatives:
o E2E tests can sometimes produce false positives or negatives due to the complexity of the
system and external dependencies. This can lead to mistrust in the test results and
additional effort to verify the outcomes.
Purpose Validates that each unit Ensures that different Validates the end-to-
functions correctly in modules or services end functionality and
isolation. It ensures work together correctly. performance of the
that individual It identifies defects in system. It ensures that
components perform the interactions the complete system
as expected. between integrated behaves as expected
units. under real-world
scenarios.
Level of Detail Highly detailed and Less detailed than unit Broad and
granular. Tests are tests but more focused comprehensive. Tests
specific to individual on the interactions the overall functionality,
units and their internal between units. Tests performance, and
logic. the communication and behavior of the entire
data exchange between system, covering all
modules. integrated components
and their interactions.
Testing Tools and Uses unit testing May use integration Uses a variety of testing
Techniques frameworks such as testing tools and tools and techniques,
Xunit, JUnit, NUnit, etc. frameworks, and often including automated
Focuses on automated involves combining testing frameworks,
tests. automated and manual functional testing,
testing. performance testing,
and user acceptance
testing.
• This is the login registration function. When we register with different permissions, different
interfaces will be displayed.
Figure 75 Create Student form
• This is the function to add a new student. When you enter all the information and click Add, you
will jump to the interface displaying the list of students just added.
Figure 77 Edit student form
• This is the editing function. When you click on the edit button, it will jump to the editing interface.
We can change the data fields as desired. Once entered, click save and the changes will
automatically return to the list display interface.
Figure 79 Student list function delete
• The last function is to delete. When you click delete, it will be deleted and updated immediately
in the list display interface
• In addition to the student management page, there are also course management, teacher
management, and classroom management pages with add, edit, and delete functions similar to
student management.
2. Test
a) TestLoadCourseFromFile
Detailed Breakdown
1. Arrange
Define File Name: Specify the name of the JSON file that will be used.
Create JSON Data: Define a JSON string representing an array of courses. This JSON string contains two
course objects.
Write JSON Data to File: Write the JSON string to a file with the specified file name.
2. Act
• Call the Method Under Test: Invoke the LoadCourseFromFile method with the file name.
3. Assert
In the assert section, we verify that the method's behavior matches our expectations:
Check Result is Not Null: Ensure that the result of the method call is not null.
Check Number of Courses: Verify that the result contains exactly 2 courses.
Check Lecturer: Ensure the second course's Lecturer is "Nguyen Thanh Trieu".
Conclusion
This test is crucial for validating the data loading functionality, ensuring that any changes to the
LoadCourseFromFile method do not break its expected behavior.
If the JSON format in the file is incorrect or malformed, the test will fail. This could happen if there are
syntax errors in the JSON string.
Example:
Let's say the JSON string is missing a closing brace or contains an extra comma:
Result:
In this case, the LoadCourseFromFile method might throw an exception while trying to parse the JSON,
resulting in a test failure.
If the JSON data contains incorrect data types for the fields, the test will fail during deserialization or
assertion.
Example:
Result:
The test will fail because the Id field is expected to be an integer, and the deserialization process will not
be able to parse it correctly.
b) TestCreateCourse
Detailed Breakdown
1. Arrange
• Controller Instance:
• Course to Add:
• List of Classes:
2. Act
3. Assert
• Redirect to "ManageCourse":
If the CreateCourse method does not redirect to "ManageCourse", the test will fail.
Example:
Result:
c) TestDeleteCourse
1. Arrange
2. Act
3. Assert
If the JSON file cannot be found or read, the method might fail to load the courses, causing the test to
fail.
Example:
Result:
The test will fail at the file reading and deserialization steps:
If there is an error in the JSON format or deserialization process, the test might fail.
Example:
Result:
• How to apply:
o StudentService is only responsible for CRUD (Create, Read, Update, Delete) operations related
to Student.
o Easy to maintain:
➢ When you need to change the Student processing logic, you only need to focus on the
StudentService class.
➢ For example: If you want to add a log feature every time there is an operation with
Student, you only need to make changes in StudentService, without affecting
StudentController or other parts.
➢ Changes in StudentService are less likely to affect other parts of the application.
➢ For example: If you change the way of storing data from JSON to database, just make
changes in StudentService, StudentController will still work normally.
o Easy to reuse:
• How to apply:
• Specific effects:
o Easy expansion:
➢ If in the future you want to add a new way of handling Student (e.g. storage in the
cloud), you can create a new class implementing `IstudentService` without modifying
`StudentController`.
o Flexibility in testing:
➢ You can easily create a mock object for `IstudentService` to test `StudentController`
without needing a real implementation.
• How to apply:
• Specific effects:
➢ If in the future you add new methods to `IStudentService`, `StudentController` will not be
affected.
How to Apply:
o If a method is expected to return a list of courses or delete a course, the behavior should
be consistent across all implementations.
3. Use Dependency Injection:
o In the CourseController constructor, dependencies are injected via interfaces. This allows
you to easily swap different implementations of the repositories without altering the
controller's code.
4. Test Substitutability:
o Ensure that all implementations of ICourseRepository can replace each other in the
CourseController without causing any errors or unexpected behavior.
Specific Effects:
1. Improved Flexibility:
2. Enhanced Reusability:
o By adhering to LSP, you can use mock implementations of ICourseRepository to unit test
the CourseController. This isolates the controller's logic from the underlying data storage
mechanism, simplifying testing.
5. Easier to Extend:
o Adding new functionalities, like logging or caching, can be done by creating new
implementations of ICourseRepository without modifying existing classes. This adheres to
both the Open/Closed Principle and LSP.
e) Dependency Inversion Principle (DIP):
• How to apply:
• Specific effects:
➢ For example: Switching from JSON storage to SQL database only requires changes in
StudentService or creating a new implementation, StudentController still works normally.
➢ You can easily create mock objects for IStudentService and IEmailValidator to test
StudentController independently.
o Reduce coupling:
Advantages
• Early Bug Detection: Helps find bugs early in the development process.
• Isolated Testing: Tests individual units in isolation, which makes debugging easier.
• Encourages Better Design: Writing tests often leads to more modular and cleaner code.
Disadvantages
• False Sense of Security: Passing unit tests doesn’t guarantee the overall system’s correctness.
Benefits
Challenges
Integration Testing
Advantages
• Improved System Reliability: Ensures that integrated components work well together.
Disadvantages
• Resource Intensive: Requires more resources and infrastructure to test integrated systems.
Benefits
Challenges
End-to-End Testing
Advantages
• Comprehensive Testing: Tests the entire application, including front-end and back-end.
• High Bug Detection: Detects issues that affect the entire system, including UI, database, and
network interactions.
Disadvantages
• Fragility: Tests can be brittle and fail due to minor changes in the application.
Benefits
• User Experience Validation: Ensures that the application works as expected from a user’s
perspective.
• Comprehensive Coverage: Provides coverage for all parts of the system, reducing the risk of
undetected bugs.
Challenges
• Maintenance Overhead: Requires constant updates to keep tests aligned with the application
changes.
• Complex Setup: Setting up and managing test data, state, and configurations is challenging.
Execution Speed Very fast, typically Moderate speed, Slowest, can take
milliseconds per test typically seconds to several minutes to run
minutes per test full system tests
Examples Testing a function that Testing if the login Testing a user’s journey
calculates the sum of module correctly from logging in to
two numbers interacts with the user making a purchase
database
Environment Setup Minimal setup, often Requires setup of Requires a full system
uses mocks/stubs multiple modules and environment that
their interactions mimics production
Testing Tools JUnit, pytest, NUnit Postman, TestNG, JUnit Selenium, Cypress,
with integration Puppeteer
libraries
Test Coverage Focus Code correctness of Interaction correctness Overall system behavior
individual units between units and user experience
Feedback Speed Immediate feedback Moderate feedback Slower feedback,
for developers during speed usually part of the final
development testing phase
Example Languages Python, Java, C# Java, Python, JavaScript Any language that
interacts with the entire
system
Reliability High for individual High for module High for overall system
units, low for system interactions, moderate reliability and user
reliability for overall system satisfaction
Purpose
• Unit Testing focuses on ensuring that individual functions or methods work correctly in isolation.
• Integration Testing ensures that different modules or components work together as expected.
• End-to-End Testing verifies that the entire system works correctly from the user's perspective.
Scope
• End-to-End Testing covers the entire application, including user interface and backend.
Complexity
• Integration Testing is more complex due to the interactions between different modules.
• End-to-End Testing is the most complex, testing complete user workflows and system behavior.
Execution Speed
• Unit Testing is very fast, as it only tests small, isolated pieces of code.
• Integration Testing is moderately fast, but slower than unit testing due to the additional
complexity.
• End-to-End Testing is the slowest, as it tests the entire system and mimics real user interactions.
Cost
• Unit Testing is low-cost due to its simplicity and minimal resource requirements.
• Integration Testing has a moderate cost, requiring more resources to test module interactions.
• End-to-End Testing is high-cost due to the need for extensive resources and complex setup.
Bug Detection
• Unit Testing is effective for catching simple bugs early in the development process.
• Integration Testing is good for identifying issues in module interfaces and interactions.
• End-to-End Testing is comprehensive, catching system-wide issues, including user interface and
backend interactions.
Maintenance
• Unit Testing is easy to maintain, with updates needed only when individual units change.
• End-to-End Testing is challenging to maintain, needing updates for any changes in user workflows
or system logic.
Automation
• Unit Testing is easily automated and typically integrated into CI/CD pipelines.
• End-to-End Testing is the most difficult to automate, needing robust tools and comprehensive
setup.
Environment Setup
• Unit Testing requires minimal setup, often using mocks and stubs.
• Integration Testing needs a setup that includes multiple modules and their interactions.
• End-to-End Testing demands a full system environment that closely resembles production.
Testing Tools
• Unit Testing tools include JUnit (Java), pytest (Python), and NUnit (C#).
• Integration Testing tools include Postman for API testing, TestNG, and JUnit with integration
libraries.
Feedback Speed
• End-to-End Testing provides slower feedback, usually as part of the final testing phase.
Example Languages
• Unit Testing can be done in any programming language, like Python, Java, or C#.
• Integration Testing is commonly done in languages used for the development of interacting
modules, like Java, Python, or JavaScript.
• End-to-End Testing is done in languages that can interact with the entire system.
Development Integration
• Unit Testing is strongly integrated into the development process, often written alongside the
code.
Reliability
• Unit Testing provides high reliability for individual units but does not guarantee overall system
reliability.
• Integration Testing ensures high reliability for module interactions and moderate reliability for
the overall system.
• End-to-End Testing ensures high reliability for the entire system and user satisfaction.
III. CONCLUSION
This report has provided a thorough exploration of essential software design principles and testing
methodologies, demonstrating their critical importance in the development of high-quality software
systems. By investigating the object-oriented paradigm, class relationships, and SOLID principles, as well
as clean coding techniques, the first part of the report underscores how these concepts enhance system
quality and maintainability, particularly through their application in the design and development of the
SIMS architecture.
The second part of the report highlights the various forms of testing—automation, unit, integration, and
end-to-end—and their roles in ensuring software reliability and performance. By evaluating the
effectiveness of SOLID principles and clean coding techniques, along with the advantages and limitations
of automated testing, the report offers valuable insights into the best practices for software
development and testing.
In conclusion, the integration of sound design principles with effective testing strategies is not just a
theoretical exercise but a practical necessity for producing resilient, efficient, and scalable software
systems. The findings and analyses presented in this report emphasize the importance of continuous
learning and adherence to established design and testing methodologies, which ultimately contribute to
the successful realization of robust and reliable software solutions.
IV. REFERENCES
[1] Admin (2023) What are the six types of relationships in UML class diagrams?, Visual Paradigm Blog.
Available at: https://blog.visual-paradigm.com/what-are-the-six-types-of-relationships-in-uml-class-
diagrams/ (Accessed: 22 July 2024).
[2] Malshika, A.Y. (2024) Mastering solid principles in C#: A practical guide, Syncfusion. Available at:
https://www.syncfusion.com/blogs/post/mastering-solid-principles-csharp (Accessed: 22 July 2024).
[4] Opoku, I.C. (2024) How to use object-oriented programming in C# – explained with examples,
freeCodeCamp.org. Available at: https://www.freecodecamp.org/news/how-to-use-oop-in-c-
sharp/#polymorphism (Accessed: 22 July 2024).
[7] AWS (2024) What is unit testing? - unit testing explained - AWS. Available at:
https://aws.amazon.com/what-is/unit-testing/ (Accessed: 06 August 2024).
[8] Katalon (2023) What is integration testing? definition, examples, how-to, katalon.com. Available at:
https://katalon.com/resources-center/blog/integration-testing (Accessed: 07 August 2024).
[9] Team, C. (2023) What is automation testing? Ultimate Guide & Best Practices, katalon.com. Available
at: https://katalon.com/resources-center/blog/what-is-automation-testing (Accessed: 07 August 2024).
My Project:
https://github.com/quanla001/APDP_ASM2