Introduction
This week was another productive part of my #100DaysOfCode journey, with a mix of learning, revision, and building.
I started the week by going deeper into software testing with JUnit 5, exploring advanced features such as dynamic tests, parameterized tests, test templates, dependency injection, and JUnit's Extension Model. I also learned how JUnit 5 integrates with tools and frameworks like Mockito, Spring, Selenium, and Cucumber.
I then spent time revising test case design and testing management, covering topics such as test planning, test fixtures, equivalence partitioning, boundary value analysis, code coverage, TDD, BDD, continuous integration, and testing pipelines.
One of the biggest highlights of the week was completing my Mastering Software Testing with JUnit 5 exam, where I scored 70%. It was a valuable learning experience, and I'm taking the lessons from the course forward as I continue improving my Java and backend development skills.
After completing the course, I shifted my focus toward building. I started a new portfolio from scratch using Next.js, Tailwind CSS, and Motion, where I worked on the Hero and About sections.
Alongside the portfolio work, I also started reading Introduction to Docker and began refreshing my understanding of containerization.
This week reminded me that growth as a developer isn't only about learning new technologies. It's also about revising what I've learned, putting it into practice, and continuing to build.
With only a few days left in the challenge, I'm focused on making the most of them.
"Great things are done by a series of small things brought together."
— Vincent van Gogh
Day 87: Going Deeper into JUnit 5
As part of my #100DaysOfCode journey, I continued learning JUnit 5 and went deeper into how testing works and how JUnit helps developers write, organize, and maintain tests.
At first, testing can seem straightforward: write some code, create a test, run it, and check whether it passes. But as applications grow, tests also need to be properly organized and managed.
This was one of the things I started understanding better during this part of my JUnit 5 learning.
Understanding the Test Lifecycle
One of the topics I explored was the JUnit 5 test lifecycle.
JUnit provides annotations that allow us to run specific pieces of code before and after tests.
Some of the annotations I worked with include:
-
@BeforeEach— runs before each test method. -
@AfterEach— runs after each test method. -
@BeforeAll— runs once before all tests in a test class. -
@AfterAll— runs once after all tests in a test class.
These annotations are useful when a test needs some setup or cleanup.
For example, if multiple tests require a particular object or resource, instead of repeating the setup inside every test, the setup can be handled using @BeforeEach.
This helps keep the tests cleaner and easier to maintain.
Test Instance Lifecycle
I also learned about the test instance lifecycle and the @TestInstance annotation.
JUnit 5 normally creates a new instance of the test class for each test method. However, @TestInstance can be used to change this behavior.
This becomes particularly useful when working with shared test state or when using lifecycle methods such as @BeforeAll and @AfterAll in certain situations.
It was another example of how JUnit 5 gives developers more control over how their tests are executed.
Working with Assertions
Assertions are one of the most important parts of writing tests because they allow us to verify whether the actual result matches what we expect.
I worked with several JUnit 5 assertions, including:
assertEqualsassertThrowsassertAllassertTimeout
For example, assertEquals can be used to check whether two values are equal, while assertThrows allows us to verify that a particular piece of code throws the expected exception.
I found assertAll particularly useful because it allows multiple assertions to be grouped together, while assertTimeout can be used when the execution time of a test is important.
Understanding these assertions helped me see that testing isn't only about checking whether a method returns the expected value. We can also test exceptions, multiple conditions, and execution time.
Organizing Tests with @Nested and @Tag
As the number of tests in a project increases, organization becomes important.
JUnit 5 provides annotations such as @Nested and @Tag to help with this.
@Nested allows related tests to be grouped together inside nested test classes. This can make larger test classes easier to understand.
@Tag, on the other hand, allows tests to be categorized.
For example, tests could be tagged as:
@Tag("unit")
or:
@Tag("integration")
This makes it possible to organize tests based on their purpose and selectively run certain groups of tests.
Conditional Test Execution and Assumptions
Another interesting area was conditional test execution.
Sometimes, a test should only run under certain conditions. JUnit 5 provides ways to control this using conditions and assumptions.
Assumptions are useful when a test depends on something that may not always be available.
For example, a test might only make sense when a particular environment variable is present or when the application is running under a specific condition.
Instead of simply allowing the test to fail, assumptions can tell JUnit that the test should be skipped when the required condition isn't met.
Repeated Tests
I also learned about @RepeatedTest.
As the name suggests, this allows a test to be executed multiple times.
@RepeatedTest(5)
void testSomething() {
// test logic
}
In this example, JUnit will execute the test five times.
This can be useful when we want to verify that a piece of functionality consistently produces the expected result across multiple executions.
Disabling Tests
JUnit 5 also provides the @Disabled annotation for temporarily disabling a test.
@Disabled
@Test
void testSomething() {
// test logic
}
There can be situations where a test is not ready, needs to be investigated, or should temporarily be excluded from execution.
Rather than deleting the test completely, it can be disabled and revisited later.
JUnit 4 Migration Support
Another topic I explored was JUnit 4 migration support.
JUnit 5 introduced several changes compared to JUnit 4, so understanding how existing JUnit 4 tests can work with newer versions is important, especially when working on existing applications.
This gave me a better understanding of how developers can gradually move from older testing approaches to JUnit 5 without necessarily having to rewrite everything at once.
What I Learned
This part of my JUnit 5 learning helped me understand that writing tests is not just about creating assertions.
There is also a lot that goes into managing a test suite:
- Setting up and cleaning up test resources
- Controlling the test lifecycle
- Writing different types of assertions
- Grouping and organizing tests
- Running tests conditionally
- Repeating tests when necessary
- Temporarily disabling tests
- Supporting migration from older versions of JUnit
JUnit 5 provides a lot of flexibility for handling these situations, and I'm starting to understand why having a well-organized testing strategy becomes increasingly important as a project grows.
Day 88: Exploring Advanced JUnit 5 Features
After going deeper into JUnit 5's testing fundamentals, I continued with Chapter 4: Simplifying Testing With Advanced JUnit Features.
This chapter introduced me to several features that make JUnit 5 more flexible and powerful, especially when tests need to be reused, generated dynamically, or executed with different sets of data.
Dependency Injection in JUnit 5
One of the first concepts I explored was Dependency Injection in JUnit 5.
JUnit 5 supports dependency injection through its built-in parameter resolvers.
Instead of manually creating everything a test method needs, JUnit can provide certain parameters automatically when the test is executed.
This was interesting to learn because dependency injection is something I've already encountered in backend development, particularly when working with frameworks like Spring.
Seeing a similar concept being used within JUnit helped me understand how the framework manages test execution and dependencies.
Dynamic Tests
One of the topics that stood out to me the most was Dynamic Tests.
JUnit 5 provides the @TestFactory annotation for creating dynamic tests.
A @TestFactory method is different from a normal test method. It isn't itself a test. Instead, it acts as a factory that produces tests at runtime.
For example:
@TestFactory
Collection<DynamicTest> dynamicTests() {
return Arrays.asList(
DynamicTest.dynamicTest("Test 1", () -> {
// test logic
}),
DynamicTest.dynamicTest("Test 2", () -> {
// test logic
})
);
}
This approach can be useful when the tests need to be generated based on data or conditions that aren't known until runtime.
It was one of those features that made me realize how much control JUnit 5 gives developers over test creation and execution.
Sharing Test Semantics with Java Interfaces
I also explored how Java interfaces can be used to share JUnit test semantics.
JUnit 5 allows test-related annotations and behavior to be defined in interfaces and then reused by implementing classes.
This can help reduce duplication when multiple test classes need to follow the same testing structure or conventions.
It was another example of how Java's existing features can work together with JUnit to create reusable testing approaches.
Test Templates
Another feature I learned about was Test Templates.
A test template provides a way to define a test structure that can be invoked multiple times with different contexts or configurations.
Unlike a normal test that runs in a fixed way, a test template provides a more reusable structure for tests that need to be executed under different conditions.
This becomes especially useful when the same testing logic needs to be applied across different scenarios.
Parameterized Tests
I also spent time learning about Parameterized Tests.
Parameterized tests allow the same test logic to be executed multiple times using different input values.
Instead of writing separate test methods for every input, we can write one test and provide multiple arguments.
For example:
@ParameterizedTest
@ValueSource(strings = {"Java", "JUnit", "Spring"})
void testSomething(String value) {
// test logic
}
The test can then be executed using each value provided by @ValueSource.
This can significantly reduce duplicated test code.
Argument Providers
JUnit 5 provides several ways to supply arguments to parameterized tests.
Some of the argument sources I explored include:
@ValueSource@EnumSource@MethodSource@CsvSource@CsvFileSource@ArgumentsSource
Each one provides a different way of supplying test data.
For simple values, @ValueSource can be enough.
For more complex or reusable data, @MethodSource or a custom @ArgumentsSource can be useful.
@CsvSource and @CsvFileSource also make it possible to provide multiple values in a CSV-style format.
Understanding these different options helped me see how parameterized tests can make a test suite much more concise while still covering many different scenarios.
Argument Conversion
Another area I explored was argument conversion.
When using parameterized tests, the values provided to a test don't always have to be the exact same type as the method parameters.
JUnit 5 can perform certain conversions automatically, and developers can also define custom conversions when necessary.
This makes parameterized tests more flexible when working with different types of test data.
Custom Parameterized Test Names
I also learned that parameterized tests can have custom display names.
This is useful because when many parameterized tests are executed, the output can become difficult to understand.
Custom names can make it clearer which input values were used for a particular test execution.
For example, instead of seeing several executions with a generic test name, we can include the supplied arguments in the display name.
This makes test reports easier to read and understand.
What I Learned
Day 88 introduced me to some of the more advanced features of JUnit 5.
The biggest takeaway for me was that JUnit isn't just about writing simple test methods and assertions. It provides tools for creating flexible, reusable, data-driven, and dynamic tests.
The features I explored included:
- Dependency Injection and parameter resolvers
- Dynamic Tests with
@TestFactory - Sharing test semantics through Java interfaces
- Test Templates
- Parameterized Tests
- Different argument providers
- Argument conversion
- Custom parameterized test names
I'm gradually getting a better understanding of how these features can be applied when building larger and more maintainable test suites.
There is still more to learn, but each chapter is making JUnit 5 feel a lot less unfamiliar.
Day 89: Integrating JUnit 5 with External Frameworks
After exploring JUnit 5's advanced testing features, I continued with Mastering Software Testing with JUnit 5, focusing on how JUnit 5 integrates with external frameworks and tools.
This part was interesting because it showed me that JUnit 5 doesn't have to work alone. Its Extension Model allows it to integrate with different frameworks and provide additional functionality depending on what we're testing.
Understanding the JUnit 5 Extension Model
One of the main concepts I explored was the JUnit 5 Extension Model.
Extensions provide a way to extend JUnit's behavior without modifying the JUnit framework itself.
This allows external tools and frameworks to hook into the test lifecycle and provide additional features.
Instead of having JUnit handle every testing requirement on its own, extensions allow other frameworks to work alongside it.
This makes JUnit 5 flexible enough to support different testing scenarios.
Mockito and Mocking
I also learned about integrating JUnit 5 with Mockito.
Mockito is commonly used for mocking and stubbing when testing Java applications.
Mocking allows us to create fake versions of dependencies so that we can test a particular component without relying on its actual dependencies.
For example, if a service depends on a database repository, we can mock the repository and control what it returns during a test.
This allows us to focus on testing the service itself rather than involving the database every time the test runs.
I also explored stubbing, which allows us to define how a mocked dependency should behave when certain methods are called.
This helped me better understand how unit tests can isolate the component being tested.
JUnit 5 and Spring
Another important integration I explored was Spring's testing support.
Since I've been working with Spring and Spring Boot on the backend side, this was particularly useful for me.
Spring provides testing features that allow JUnit tests to work with the Spring application context.
This makes it possible to test components within an environment that is closer to how the actual application runs.
It also showed me how JUnit and Spring complement each other rather than treating testing as a completely separate part of application development.
Selenium and UI Testing
I also looked at how JUnit 5 can work with Selenium.
Selenium is used for browser automation and testing web applications.
JUnit can be used to organize and execute these tests while Selenium handles the browser interaction.
This combination allows developers to automate scenarios such as opening a webpage, interacting with elements, submitting forms, and verifying results.
It was interesting to see how the same testing framework can be used alongside tools that operate at different levels of an application.
Cucumber Integration
Another framework I explored was Cucumber.
Cucumber supports behavior-driven development (BDD), where application behavior can be described in a more human-readable format.
JUnit 5 can integrate with Cucumber, allowing these scenarios to be executed as part of the testing process.
This highlighted another important aspect of testing: tests don't always have to focus only on individual methods or classes. They can also describe and verify application behavior from a broader perspective.
Testing REST APIs
I also explored different approaches to testing REST APIs.
For backend development, API testing is particularly important because APIs are often the main way that different parts of an application communicate.
JUnit can be used alongside other tools and libraries to verify things such as:
- HTTP status codes
- Request and response data
- API behavior
- Error handling
- Different input scenarios
This is especially relevant to the kind of backend development I'm working toward, where building and testing reliable APIs is an important part of the job.
Testing Docker Containers
Another area covered was testing applications running in Docker containers.
Docker makes it possible to package an application and its dependencies into a consistent environment.
Testing containerized applications introduces additional considerations because we're no longer testing only the application code. We may also need to verify how the application behaves within its containerized environment.
Learning about this connection between testing and Docker was useful as I'm also gradually learning more about containerization and deployment.
Android Application Testing
The chapter also introduced testing approaches for Android applications.
Although Android development isn't my primary focus at the moment, it was useful to see that JUnit 5 and its testing concepts can be applied across different types of software and environments.
It reinforced the idea that testing principles aren't limited to backend applications.
What I Learned
Day 89 helped me understand the importance of JUnit 5's ability to work with other tools and frameworks.
The Extension Model provides a foundation for integrating JUnit with different technologies, while frameworks such as Mockito, Spring, Selenium, and Cucumber can address specific testing requirements.
Some of the areas I explored included:
- JUnit 5's Extension Model
- Mocking and stubbing with Mockito
- Spring testing support
- Selenium integration
- Cucumber and BDD
- REST API testing
- Docker container testing
- Android application testing
The biggest takeaway for me is that testing doesn't happen in isolation.
Depending on the application, developers may need different tools to test different parts of the system. JUnit 5 provides a foundation that can work with these tools and help bring the different testing approaches together.
I'm getting closer to the end of this JUnit 5 learning journey, and I'm starting to see how the concepts I've learned can fit into real backend development.
Day 90: Revising Test Case Design and Testing Management
As I got closer to completing my Mastering Software Testing with JUnit 5 course, Day 90 was focused mainly on revision.
I went back through Chapters 6 and 7 to strengthen my understanding of the concepts before taking the exam the following day.
The revision covered two major areas: test case design and testing management.
Revisiting Test Case Design
The first part of my revision focused on test case design.
Writing tests isn't simply about creating a test method and checking whether it passes. A good testing process starts with understanding what needs to be tested and designing test cases that can effectively identify potential problems.
Test Planning
I revisited the importance of test planning.
Before writing tests, it's important to understand the requirements, identify what needs to be tested, determine the appropriate testing approach, and consider the resources and conditions required.
Good planning provides a foundation for the testing process and helps ensure that important scenarios aren't overlooked.
Test Fixtures
I also reviewed test fixtures.
A test fixture refers to the setup and state required for a test to run.
This includes things such as creating objects, preparing test data, initializing resources, and cleaning up after tests.
This connected nicely with some of the JUnit 5 concepts I had already learned, particularly the test lifecycle annotations such as @BeforeEach and @AfterEach.
Equivalence Partitioning
Another test case design technique I revisited was equivalence partitioning.
The idea is to divide input data into groups, or partitions, where the values in each group are expected to behave similarly.
Instead of testing every possible input, we can select representative values from each partition.
For example, if an application accepts ages from 18 to 60, we could divide the inputs into categories such as:
- Below 18
- 18–60
- Above 60
Rather than testing every possible age, we can select representative values from each group.
This can help reduce the number of test cases while still providing useful coverage.
Boundary Value Analysis
I also revisited boundary value analysis.
This technique focuses on the edges of valid and invalid input ranges.
Using the same example of an age range from 18 to 60, instead of only testing values in the middle of the range, we would pay particular attention to values such as:
- 17
- 18
- 19
- 59
- 60
- 61
Boundary values are important because errors often occur around the limits of accepted input.
This was a good reminder that choosing test data carefully can be just as important as writing the test itself.
Code Coverage
Another topic I reviewed was code coverage.
Code coverage helps us understand how much of our code is being executed by our tests.
There are different types of coverage, including:
- Line coverage
- Branch coverage
- Method coverage
- Condition coverage
However, high code coverage doesn't automatically mean that an application is well tested.
A test suite could execute a large percentage of the code while still failing to properly verify important behaviors.
For me, this was an important distinction: coverage is a useful measurement, but it isn't the same thing as test quality.
Testing Management
The second major area I revised was testing management.
This section moved beyond individual test cases and looked at how testing fits into the wider software development process.
Test-Driven Development
I revisited Test-Driven Development (TDD).
TDD follows a cycle commonly described as:
Red → Green → Refactor
The developer first writes a test that fails, then writes enough code to make the test pass, and finally improves the implementation while keeping the tests passing.
The idea isn't simply to write more tests. It's about using tests as part of the development process itself.
Behavior-Driven Development
I also reviewed Behavior-Driven Development (BDD).
BDD focuses more on describing how a system should behave from a user's or business perspective.
This can make requirements easier for developers, testers, and non-technical stakeholders to understand and discuss.
It also connected with what I learned earlier about Cucumber and its approach to describing application behavior.
Continuous Integration
Another important topic was Continuous Integration (CI).
Instead of waiting until the end of development to run tests, CI allows tests to be executed automatically whenever changes are integrated into a shared codebase.
This can help developers detect problems earlier.
For example, a typical pipeline might:
- Pull the latest code.
- Build the application.
- Run automated tests.
- Generate test reports.
- Report failures.
This connects testing directly with the development workflow rather than treating it as a separate final step.
Testing Pipelines
I also revised testing pipelines and how automated testing can become part of a larger software delivery process.
A testing pipeline can include different stages depending on the project, such as unit tests, integration tests, API tests, and other automated checks.
The goal is to provide continuous feedback about the health of the application as changes are introduced.
This is particularly relevant to backend development and deployment, where automated testing can help catch problems before new code reaches production.
Test Reporting and Defect Tracking
Finally, I looked at tools and practices for test reporting and defect tracking.
Writing and running tests is only part of the testing process. Teams also need ways to understand test results, document failures, and track defects through to resolution.
Test reports can provide useful information about what passed, what failed, and where problems occurred.
Defect tracking systems then help teams record and manage those issues as part of the development workflow.
What I Learned
Day 90 was less about learning completely new concepts and more about connecting everything I had already studied.
I revisited:
- Test planning
- Test fixtures
- Equivalence partitioning
- Boundary value analysis
- Code coverage
- TDD
- BDD
- Continuous integration
- Testing pipelines
- Test reporting
- Defect tracking
One of my biggest takeaways from the revision was that effective software testing involves much more than writing assertions.
There is the design of the test cases, the quality of the test data, the development process, automation, reporting, and how testing fits into the overall software delivery lifecycle.
After spending the previous days exploring JUnit 5 and its integrations, this revision helped bring many of the concepts together.
Day 91: Completing My JUnit 5 Journey
After several days of learning, practicing, and revising, I finally completed my Mastering Software Testing with JUnit 5 exam.
I scored 70%. 🎉
It was a good way to wrap up the learning experience and see how much I had picked up throughout the course.
What I Learned
Throughout the course, I explored different areas of software testing, starting from the fundamentals and gradually moving into more advanced JUnit 5 concepts.
Some of the major topics I covered include:
- Software testing fundamentals
- Test case design
- JUnit 5 features and test lifecycle
- Assertions and parameterized tests
- Dynamic tests and test templates
- JUnit 5's Extension Model
- Mocking and stubbing with Mockito
- Spring testing support
- REST API testing
- Testing management
- TDD and BDD
- Continuous integration and testing pipelines
- Test reporting and defect tracking
One thing I appreciated about the course was that it went beyond simply teaching me how to write JUnit tests.
It helped me understand where testing fits into the software development lifecycle and how different testing tools and practices can work together.
Moving Forward
Although the course is now completed, I don't see this as the end of learning about software testing.
There are still concepts I want to practice through real projects, especially writing meaningful tests for backend applications and APIs.
I'm taking what I've learned forward as I continue improving my Java and backend development skills.
With the exam completed, I can now shift more of my attention toward building and applying what I've learned.
And with 9 days left in the #100DaysOfCode challenge, there's still more work to do.
Day 92: Starting a New Portfolio
With my JUnit 5 learning journey completed, I decided to shift my focus back toward building.
For Day 92, I started working on a new portfolio website from scratch using Next.js, Tailwind CSS, and Motion.
I've built a portfolio before, but this time I wanted to start fresh and create something that better represents my current skills, projects, and growth as a developer.
Starting with the Foundation
I started by setting up the project and working on the first major sections of the portfolio.
The first sections I worked on were:
- Hero Section
- About Section
The Hero section is the first thing visitors will see, so I wanted it to clearly communicate who I am and what I do.
For the About section, I'm working on presenting my background and development journey in a simple way.
I'm still early in the build, so there is a lot more to come.
Next.js and Tailwind CSS
For this portfolio, I'm using Next.js as the main framework and Tailwind CSS for styling.
I've worked with React before, but using Next.js for the project gives me another opportunity to strengthen my understanding of the framework while building something I'll actually use.
Tailwind CSS is also helping me move quickly while keeping the styling within the components.
Adding Motion
I'm also using Motion to add animations and interactions to the portfolio.
I don't want the animations to simply exist for the sake of animation. I'm experimenting with how subtle movement can make sections feel more interactive without taking attention away from the actual content.
This is something I'll continue refining as the portfolio develops.
Starting Docker
Alongside the portfolio work, I also started reading Introduction to Docker.
After spending so much time learning about backend development, testing, and different parts of the software development lifecycle, I want to become more comfortable with containerization and deployment.
Docker is an important part of modern development workflows, so I'm taking some time to understand the fundamentals before moving into more practical usage.
For now, I'm focusing on understanding the concepts rather than trying to rush through everything.
What's Next?
The portfolio is still a work in progress.
There are several sections I still want to build, improve, and connect before I consider it finished.
I'll also continue learning Docker alongside the development work.
One thing I've noticed throughout this challenge is that learning doesn't always happen through one activity.
Sometimes I'm reading.
Sometimes I'm building.
Sometimes I'm debugging.
And sometimes I'm going back to something I've already learned and applying it in a different way.
Day 92 was a combination of all three.
8 days left in the challenge.
Goals for Week 15
As I move into Week 15, my focus will shift toward building, improving my portfolio, and strengthening my understanding of Docker, while continuing to develop my backend engineering skills.
Here are my goals for Week 15:
Continue building my new portfolio with Next.js, Tailwind CSS, and Motion
Complete and improve the main sections of my portfolio website
Continue learning the fundamentals of Docker and containerization
Strengthen my understanding of Java and backend development
Apply what I've learned through practical projects and hands-on development
Practice LeetCode when I have available time
Continue applying for backend engineering opportunities
Stay consistent with #100DaysOfCode and continue documenting my progress
With the challenge getting closer to the finish line, I want to make the most of this week by building more, applying what I've learned, and strengthening the skills I need as a backend-focused full-stack developer.
I'm looking forward to making progress on my portfolio while continuing to learn Docker and improve my development workflow.
Connect With Me
LinkedIn:
https://www.linkedin.com/in/onatade-abdulmajeed/
X (Twitter):
https://x.com/spider337761








