Sitemap
iamprovidence

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence

Code Testing

12 min readAug 7, 2025

--

Press enter or click to view image in full size

Today we’ll talk about testing. It’s a broad topic with tons of tools, frameworks, and approaches — way too much to cover in one go. So this time, we’ll just scratch the surface and try to get a general overview.

If you are interested, do not forget to check out other articles:

Meanwhile, we are starting 🧪

Understanding Tests

We write code to solve problems — but how can we be sure that code actually works 😅? That’s where tests come in.

Tests are also code that verifies our program behaves as expected. They catch bugs early and give us confidence to refactor or extend functionality.

Since tests are code, they also require time to write, maintain, and keep up-to-date. Just like production code, tests can be poorly written, unnecessary, or even overused. Remember that it’s better not to write a test at all than to write a bad one.

Why Do We Need Tests?

There are multiple reasons why you would need to write tests:

  • tests catch bugs early, before they reach production, saving time and money
  • tests force developers to write high-quality code with clear structure, better separation of concerns, and fewer side effects
  • it gives us confidence that our changes don’t break existing functionality, especially during refactoring
  • testing helps us ensure that our code works correctly — not just once, but every time it runs
  • they also serve as living documentation, showing how the code is expected to behave
Press enter or click to view image in full size

What to Test?

In your projects, there are two types of requirements:

Press enter or click to view image in full size

The sweetest part here is that everything can be tested😋.

Tests can vary by purpose, defining what aspect of the system is being tested. They can be categorized as functional and system.

Press enter or click to view image in full size

Functional tests verify business logic (functional requirements).
System tests check how the system behaves under certain conditions (non-functional requirements).

Let’s quickly review each type of test.

— unit tests
Unit tests are the smallest and basic tests. They validate individual functions or methods in isolation.

Press enter or click to view image in full size

Here’s a simple example of a unit test in C#:

[Test]
public void Add_ReturnsSumOfTwoNumbers()
{
var result = Math.Add(2, 3);
result.Should().Be(5);
}

— integration testing

Test how components work together (e.g., service and database).

Press enter or click to view image in full size

In the code, they are similar to unit tests, but test against real implementations of dependencies, such as databases, APIs, or file systems, rather than mocks or stubs.

[Test]
public void UserService_SaveUserInTheDatabase()
{
// Arrange
var userService = new UserService();

// Act
userService.AddUser(new User { Name = "Alice" });

// Assert
Db.Users.Should().NotBeNull();
Db.Users.First().Should().Be("Alice");
}

— E2E tests (acceptance tests)

Simulate user flows across the whole system, including UI, server, and database.

Press enter or click to view image in full size

Similar to what we saw before, E2E tests are automated tests that typically open a browser and interact with web pages.

[Test]
public void SignUpPage_WhenSubmitted_ShouldSaveUserInTheDatabase()
{
// Arrange
Browser.OpenPage("sign-up");

// Act
Browser.EnterField("email", "alice@mail.com");
Browser.EnterField("password", "Pass@word");
Browser.ClickButton("#submit");

// Assert
Db.Users.Should().NotBeNull();
Db.Users.First().Should().Be("alice@mail.com");
}

— connector testing

Connector tests are a specific type of integration test that focuses on verifying the interaction between your system and external services, such as third-party APIs, message queues, or payment gateways.

Press enter or click to view image in full size

They help to ensure the external API contract has not changed:

[Test]
public async Task PaymentConnector_ShouldReturnSuccessStatusFromExternalApi()
{
var paymentApiClient = new PaymentApiClient(httpClient);

var result = await paymentApiClient.ChargeAsync(100);

result.Status.Should().Be("Success");
}

— learning tests

Developers write learning tests to learn how a library, framework, or piece of unfamiliar code behaves.

Press enter or click to view image in full size

They’re not written to validate business logic like unit tests — instead, they’re used for learning and documentation.

[Test]
public void DateTime_Parse_ShouldHandleVariousDateFormats()
{
DateTime.Parse("2025-07-26").Should().Be(new DateTime(2025, 7, 26));
DateTime.Parse("07/26/2025").Should().Be(new DateTime(2025, 7, 26));
DateTime.Parse("26/07/2025").Should().Be(new DateTime(2025, 7, 26));
}

— developer testing

When the development is finished, developers start the applications and manually perform the necessary interactions to see if the code works well.

Press enter or click to view image in full size

— smoke testing

Smoke testing can be done by both developers and QA engineers.

Press enter or click to view image in full size

Instead of testing every detail, it focuses on quickly verifying that the core functionality and happy path are working as expected.

— manual testing

Manual testing is the process of testing software by QA engineers without using automation tools.

Press enter or click to view image in full size

The tester manually performs actions, verifies outputs, and checks that the system behaves as expected.

— regression testing

Regression testing is the process of re-running existing tests against the old and new systems and comparing results.

Press enter or click to view image in full size

They help to ensure that recent code changes haven’t broken previously working functionality.

— benchmark tests

Benchmark tests measure how fast a particular piece of code runs.

Press enter or click to view image in full size

They are useful for identifying performance bottlenecks or comparing implementation strategies. In C#, it is common to use BenchmarkDotNet:

// compare the performance of two algorithms
[Benchmark]
public void SortArrayV1()
{
Array.BubbleSort(data);
}

[Benchmark]
public void SortArrayV2()
{
Array.QuickSort(data);
}

— load testing

Load testing checks how the system behaves under a specific expected load (e.g., 1000 users simultaneously).

Press enter or click to view image in full size

The goal is to ensure the system remains responsive and stable. Usually conducted using tools like JMeter, K6, and NBomber.

import http from 'k6/http';
import { check } from 'k6';

export let options = {
vus: 1000, // virtual users
duration: '30s', // test duration
};

export default function () {
const res = http.get('https://yourapi.com/api/products');

check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
}

This script runs 1000 virtual users for 30 seconds and checks for status and response time.

It can be executed with:

k6 run load-test.js

— stress testing

Stress testing evaluates how the system behaves under extreme or breaking loads.

Press enter or click to view image in full size

It helps identify the system’s upper limits and how it recovers from failure.

Press enter or click to view image in full size

— spike testing

Spike testing is a form of stress testing that introduces sudden and extreme spikes in traffic.

Press enter or click to view image in full size

The goal is to observe how the system handles unexpected surges in load and whether it degrades gracefully.

Press enter or click to view image in full size

— soak testing (endurance testing)

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

Checks how the system performs over an extended period of time under a normal load.

Press enter or click to view image in full size

It’s designed to find memory leaks, slowdowns, or other issues that appear after prolonged usage.

— security testing

Security testing verifies that the system is resistant to common vulnerabilities and unauthorized access.

Press enter or click to view image in full size

It may include testing for XSS, SQL injection, CSRF, etc.

— fuzz testing

Fuzz testing involves sending random or unexpected data to the system to find crashes, assertion failures, or security holes.

Press enter or click to view image in full size

It’s especially useful for testing parsers, file readers, or APIs.

— architecture tests

Architecture tests validate architectural rules and boundaries within the codebase.

Press enter or click to view image in full size

For example, enforcing that domain logic does not depend on the infrastructure layer.

[Fact]
public void DomainLayer_ShouldNotReferenceInfrastructure()
{
var result = ArchitectureRules
.Layer("Domain")
.ShouldNotDependOn()
.Layer("Infrastructure");

result.IsSatisfied.Should().BeTrue();
}

— mutation testing

Mutation testing checks how effective your test suite is by making small changes (mutations) in the code and verifying that the tests fail.

Press enter or click to view image in full size

Stryker.NET is a popular tool for mutation testing.

Let’s say you have the following code and test:

public static class Calculator
{
public static int Add(int a, int b)
{
return a + b; // Stryker might mutate this to `a - b`
}
}

[Test]
public void Add_ReturnsCorrectSum()
{
Calculator.Add(2, 3).Should().Be(5);
}

Then you install and run the Stryker:

// install
dotnet tool install -g dotnet-stryker
// run
dotnet stryker

Stryker will report the mutation score. If the tests pass despite the mutation, it may indicate a gap in test coverage.

Testing Pyramid

While all types are great and serve their purpose, most of the time you will face unit, integration, and E2E tests.

The Testing Pyramid is a concept in software testing that illustrates how to structure and prioritize different types of automated tests for better efficiency, speed, and maintainability.

Press enter or click to view image in full size
  • unit tests (bottom layer) — test small pieces of code (like functions or methods), are fast, cheap, and easy to maintain, therefore should be written in large numbers
  • integration tests (middle layer) — test how components work together (e.g., service and database), are slower and more complex than unit tests, therefore should be used moderately
  • End-to-End tests (top layer) — test the entire system from the user’s perspective (usually through the UI), are slow, flaky, and expensive to maintain; therefore, should be written sparingly

Testing Methodologies

Different projects — and even different stages of the same project — can benefit from different testing approaches. There’s no one-size-fits-all methodology, but understanding the strengths and trade-offs of each helps you choose the right tool for the job.

Let’s review a few popular testing methodologies used by developers today.

— Test Last Development

A traditional approach that most developers adopted is to focus on the feature first and write tests later:

  • you write the implementation first, based on requirements or design
public static class Calculator
{
public static int Add(int a, int b)
{
return a + b;
}
}
  • then, when the code is completed, you write tests, often as a way to validate correctness or increase coverage
[Test]
public void Add_ReturnsCorrectSum()
{
Calculator.Add(2, 3).Should().Be(5);
}

This approach can be useful because the code structure is already established before testing begins. Since the code isn’t influenced or constrained by pre-written tests, you’re free to design it as you see fit.

Press enter or click to view image in full size

However, it can lead to fragile or incomplete tests, as they are often written after the implementation is complete — making them biased by the code rather than the intended behavior, which risks missing edge cases or regressions.

— TDD (Test Driven Development)

TDD does the opposite:

  • you write the desired API without actual implementation
public static class Calculator
{
public static int Add(int a, int b)
{
throw new NotImplementedException();
}
}
  • then you write the tests
[Test]
public void Add_ReturnsCorrectSum()
{
Calculator.Add(2, 3).Should().Be(5);
}
  • finally you write the code needed to make all tests pass
public static class Calculator
{
public static int Add(int a, int b)
{
return a + b;
}
}

TDD helps to ensure that every piece of logic is testable and has clear behavior defined up front.

It often leads to cleaner, more modular code because you’re designing from the perspective of how the code will be used, not how it will be built.

Press enter or click to view image in full size

However, it can feel restrictive or premature for high-level business logic, where the design may evolve as you explore the problem, slowing down the whole development.

— Snapshot Tests

Snapshot tests behave this way:

  • you run the test for the first time, without knowing the expected result
[Test]
public void Add_ReturnsCorrectSum()
{
var result = Calculator.Add(2, 3);

Verify(result); // save a snapshot
}

Tests capture the output, and if the developer approves it, the output is saved as the reference snapshot (usually to a file).

  • when the test runs again, it compares the current result against the saved snapshot
[Test]
public void Add_ReturnsCorrectSum()
{
var result = Calculator.Add(2, 3);

Verify(result); // compare to the saved snapshot
}
  • in case the code changes, tests will fail. The developer has to review new snapshots and decide whether those should be saved or code requires refactoring

Snapshot testing helps quickly catch unintended changes in structured output, especially for things like API responses or UI.

It reduces the need for verbose assertions and serves as a lightweight form of regression testing.

Press enter or click to view image in full size

However, over-reliance on snapshots can lead to blind approvals — developers may accept changes without fully understanding them. It works best when used selectively and with clear intent.

— BDD (Behaviour Driven Development)

Instead of thinking in terms of code, BDD focuses on behavior.

The test in this case can also be written by stakeholders:

  • anyone start by writing scenarios using a business-readable format:
Scenario: Adding two numbers
Given a calculator
When I add 2 and 3
Then the result should be 5
  • each step becomes the basis for automated tests, often using tools like SpecFlow:
[Binding]
public class CalculatorSteps
{
[Given(@"a calculator")]
public void GivenACalculator()
{
throw new NotImplementedException();
}

[When(@"I add (.*) and (.*)")]
public void WhenIAdd(int a, int b)
{
throw new NotImplementedException();
}

[Then(@"the result should be (.*)")]
public void ThenTheResultShouldBe(int expected)
{
throw new NotImplementedException();
}
}
  • the developer has to implement those steps
[Binding]
public class CalculatorSteps
{
private Calculator _calculator = null;
private int _result = null;

[Given(@"a calculator")]
public void GivenACalculator()
{
_calculator = new Calculator();
}

[When(@"I add (.*) and (.*)")]
public void WhenIAdd(int a, int b)
{
_result = _calculator.Add(a, b);
}

[Then(@"the result should be (.*)")]
public void ThenTheResultShouldBe(int expected)
{
_result.Should().Be(expected);
}
}

(notice how values are dynamically passed from the scenario into the methods via regex capture groups)

  • once implemented, anyone can reuse them to add other tests
Scenario: Adding two negative numbers
Given a calculator
When I add -1 and -9
Then the result should be -10

BDD helps create a shared language between developers, testers, and business stakeholders, aligning code with expectations.

It promotes clarity and collaboration, especially for complex business scenarios.

Press enter or click to view image in full size

However, it introduces extra tooling and ceremony, so it’s most beneficial in projects where communication and specification clarity are critical.

CI/CD

Automated tests are a core part of any modern CI/CD pipeline. They catch bugs before they reach production.

Whenever code is pushed to the repository, the CI system automatically builds the application and runs the full test suite — including unit tests, integration tests, etc.

Press enter or click to view image in full size

If any test fails, the pipeline is blocked, and the change won’t be merged or deployed until it’s fixed.

In short, tests give you confidence, and CI/CD makes sure they’re always enforced. Together, they form a feedback loop that supports stable, iterative development.

Sum It Up

So, yes, tests 😅. I think everyone knows what those are. Still having an overview of their types, methodologies, and best practices is always valuable. Understanding where they fit, how much coverage is needed, and how to maintain them efficiently can make the difference between a stable system and one that constantly surprises you — in the worst way.

Let me know, what kind of tests you face the most 💬

I know about which Xzibi would be excited😅

Press enter or click to view image in full size

Give this article a clap or a few if you like it 👏

You can support me with a link below ☕️

And don’t forget to follow if you are interested in reading about tests ✅

--

--

iamprovidence
iamprovidence

Written by iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence