LEARN BUILD GROW TOGETHER
DEVELOPER LEARNING HUB
Learn. Build. Grow Together.

Practical tutorials, real-world examples and interview preparation.

.NETC#SQL SERVERANGULAR
Home /ASP.NET Core /Software Testing: Complete Developer Learning Guide

Software Testing: Complete Developer Learning Guide

Software Testing · Beginner to Advanced

Software Testing: Complete Developer Learning Guide

Understand software testing from fundamentals to modern automation. Learn testing levels, test design, unit and integration testing, API testing, database testing, performance, security, CI pipelines, and practical testing strategies for C# and ASP.NET Core applications.

Software testing is the process of evaluating an application or one of its components to determine whether it behaves as expected and satisfies its requirements. Testing helps developers discover defects, validate business rules, reduce regression risk, and build confidence before software is released.

Testing is not limited to a separate QA phase. Developers use tests throughout the software development lifecycle: while designing a feature, implementing business logic, integrating services, reviewing changes, and preparing a release.

This guide explains general testing principles and connects them to practical .NET development. The examples use C# and common .NET tooling, but the underlying concepts apply to other programming languages and platforms.

1. Software testing fundamentals

What is software testing?

Software testing is a systematic activity used to evaluate software quality. A test supplies inputs or performs actions, observes the resulting behavior, and compares the actual result with the expected result.

For example, consider an application that calculates an order total. A test can supply item prices, quantities, and a discount, then verify that the calculated total matches the business rule.

Important testing terms

TermMeaning
TestAn execution or evaluation used to check a particular behavior or quality attribute.
Test scenarioA high-level situation or workflow that should be evaluated.
Test caseA defined set of preconditions, inputs, actions, and expected results.
Test dataInput values and supporting records used during testing.
Expected resultThe behavior or output that should occur according to the requirement.
Actual resultThe behavior or output observed when the test is performed.
Defect / bugA flaw that can cause the software to behave incorrectly or fail to meet a requirement.
RegressionAn unintended change in existing behavior after code or configuration is modified.
Test suiteA collection of related tests executed or managed together.
System under test (SUT)The application, component, or behavior being evaluated.

2. Why testing matters

Testing provides evidence about software behavior. It cannot prove that a complex application has no defects, but it can detect many problems before they affect users.

  • Find defects early: Detect incorrect calculations, invalid conditions, and unexpected behavior during development.
  • Protect existing features: Automated regression tests help reveal when a new change breaks an earlier capability.
  • Validate business rules: Confirm that the application follows the requirements and domain rules.
  • Improve maintainability: A reliable test suite makes refactoring safer and documents expected behavior.
  • Reduce release risk: Tests provide repeatable checks before deployment.
  • Support collaboration: Shared test cases and automated checks help developers, testers, analysts, and product teams agree on expected behavior.
Important: A successful test run means that the executed tests passed under the tested conditions. It does not guarantee that every possible input, environment, integration, or production scenario is defect-free.

3. Verification and validation

Verification and validation are related but different quality activities.

VerificationValidation
Checks whether the product is being built according to specified requirements and design.Checks whether the product meets user needs and intended use.
Often expressed as: “Are we building the product right?”Often expressed as: “Are we building the right product?”
Can include reviews, inspections, static analysis, and design checks.Can include executing the application, acceptance testing, and user evaluation.

For a .NET application, reviewing a method against its technical specification is a verification activity. Running the completed workflow with representative business data to confirm that it solves the user's problem is a validation activity.

4. Fundamental testing principles

  1. Testing shows the presence of defects: Tests can reveal defects, but passing tests do not prove that defects are absent.
  2. Exhaustive testing is impractical: Testing every possible input and state is usually impossible, so testing must be prioritized.
  3. Test early: Starting quality activities early can reduce the cost and impact of fixing defects.
  4. Defects cluster: A small number of components often account for a large share of observed defects. Use defect history and risk to guide attention.
  5. Tests can become stale: Repeating the same tests may stop finding new defects. Review and improve test cases as the product changes.
  6. Testing is context-dependent: A medical system, a payment service, and a content website have different risks and quality requirements.
  7. Absence-of-errors fallacy: Software can be technically free of known defects yet still fail to meet user needs.

5. Software testing lifecycle

The testing lifecycle describes the activities used to plan, design, execute, and evaluate testing. Teams may use different names or combine activities depending on their delivery process.

  1. Requirement analysis: Understand user stories, acceptance criteria, business rules, dependencies, and risks.
  2. Test planning: Define scope, approach, responsibilities, environments, tools, schedule, and completion criteria.
  3. Test design: Create test scenarios, test cases, data sets, and expected results.
  4. Environment preparation: Configure application builds, databases, credentials, services, and test data.
  5. Test execution: Run manual and automated tests and record actual results.
  6. Defect reporting: Document reproducible failures with evidence, steps, environment, and impact.
  7. Retesting and regression: Verify fixes and check that related existing behavior still works.
  8. Test closure: Summarize results, unresolved risks, coverage, and lessons learned.

In an Agile team, these activities are not necessarily completed as one large sequence. They can happen continuously within each sprint or development task.

6. Testing levels

Testing levels describe the scope at which software is evaluated. The four commonly discussed levels are unit, integration, system, and acceptance testing.

LevelFocus.NET example
UnitOne small unit of behavior in isolation.Test a C# pricing or validation service without a real database.
IntegrationInteraction between components or systems.Test an ASP.NET Core endpoint with dependency injection and a test database.
SystemThe behavior of the complete integrated application.Test a deployed application across API, database, authentication, and other services.
AcceptanceWhether the solution meets agreed business and user criteria.Validate an end-to-end business workflow against acceptance criteria.

Unit testing

Unit tests focus on a small unit of code, often a method or class. They should be fast, repeatable, and primarily exercise logic controlled by the developer. External infrastructure such as a real database or network service is usually replaced with a suitable test double.

Integration testing

Integration tests evaluate whether multiple parts work together. They may involve the ASP.NET Core request pipeline, dependency injection, serialization, database access, file storage, or external service boundaries.

System testing

System testing evaluates a complete integrated product against its functional and non-functional requirements. It can include workflows that cross multiple modules or services.

Acceptance testing

Acceptance testing determines whether the delivered capability is acceptable for its intended use. It may be performed by business representatives, product owners, customers, or an independent testing team.

7. Functional and non-functional testing

Testing can also be classified by the quality or behavior being evaluated.

Functional testing

Functional testing checks what the application does: its features, rules, inputs, outputs, and workflows.

  • Login and logout behavior.
  • Create, read, update, and delete operations.
  • Form validation and error messages.
  • Role and permission-based access.
  • Business calculations and workflow transitions.
  • API status codes, response bodies, and validation behavior.

Non-functional testing

Non-functional testing evaluates how well the system operates under defined conditions.

TypeWhat it evaluates
PerformanceResponse time, throughput, resource usage, and behavior under load.
LoadBehavior under an expected or specified workload.
StressBehavior beyond normal capacity and how the system fails or recovers.
ScalabilityAbility to handle growth in users, data, or workload as resources change.
SecurityResistance to unauthorized access, data exposure, and common vulnerabilities.
UsabilityHow effectively and comfortably users can operate the application.
AccessibilityWhether people with different abilities can use the product.
CompatibilityBehavior across supported browsers, devices, operating systems, and environments.
Reliability and recoveryConsistency over time and the ability to recover after failures.

8. Manual and automated testing

Manual testing

Manual testing is performed by a person who follows test steps, explores the application, and evaluates the results. It is useful for exploratory testing, usability feedback, visual review, and situations where human judgment is important.

Automated testing

Automated testing uses scripts and tools to execute checks and compare results. It is especially valuable for repeatable scenarios such as business-rule tests, API contracts, regression suites, and build validation.

Manual testingAutomated testing
Flexible and useful for exploration.Repeatable and suitable for frequent execution.
Can use human observation and judgment.Can provide consistent results for defined assertions.
Can become time-consuming for repeated regression.Requires initial design, implementation, and maintenance.
Useful for usability and exploratory checks.Useful for stable, high-value, repeatable checks.

Automation does not eliminate manual testing. A practical quality strategy combines automated checks with exploratory testing and human review where appropriate.

9. Test cases and test scenarios

A test scenario describes what needs to be checked at a high level. A test case defines the conditions and steps required to check it.

Example: login feature

Test scenario: Verify that users can sign in using valid credentials and that invalid attempts are handled safely.

Test caseInput / conditionExpected result
Valid loginRegistered username and correct password.User is authenticated and receives the expected destination or response.
Incorrect passwordRegistered username and incorrect password.Authentication is rejected without exposing sensitive details.
Unknown userUsername that is not registered.Authentication is rejected with an appropriately generic response.
Missing usernameUsername is empty.Input validation reports the required field.
Missing passwordPassword is empty.Input validation reports the required field.
Locked or disabled accountAccount is not allowed to sign in.Access is denied according to the account policy.

Useful test case fields

  • Test case ID and title.
  • Requirement, user story, or acceptance criterion reference.
  • Preconditions and test environment.
  • Input data and execution steps.
  • Expected result and actual result.
  • Status, evidence, priority, and defect reference where applicable.

10. Test design techniques

Test design techniques help select meaningful test inputs and scenarios without attempting every possible combination.

Equivalence partitioning

Divide input data into groups expected to behave similarly. Select representative values from each group.

For an age field that accepts values from 18 through 60, possible partitions include below 18, 18–60, and above 60. A representative test can be selected from each partition.

Boundary value analysis

Defects frequently occur at the edges of allowed ranges. Test values immediately below, at, and immediately above important boundaries.

// Example: accepted age range is 18 through 60
[Theory]
[InlineData(17, false)]
[InlineData(18, true)]
[InlineData(19, true)]
[InlineData(60, true)]
[InlineData(61, false)]
public void IsEligible_UsesExpectedAgeBoundary(
    int age,
    bool expected)
{
    var result = Eligibility.IsEligible(age);

    Assert.Equal(expected, result);
}

Decision table testing

Use a table to cover combinations of conditions and the corresponding outcomes. This is useful when business behavior depends on multiple rules, such as customer type, order value, and approval status.

State transition testing

Check how a system moves between states and whether actions are allowed in each state. For example, an order may move from Draft to Submitted, Approved, Shipped, and Completed. Tests should also verify invalid transitions.

Use-case testing

Test complete user goals and workflows, including the main success path and important alternative or failure paths.

Exploratory testing

Explore the application while learning about its behavior, designing new tests, and investigating unexpected results. Record useful observations so that important discoveries can become repeatable test cases.

11. Test pyramid and test strategy

The test pyramid is a strategy model that encourages a broad base of fast, focused tests and a smaller number of slower, broader tests.

                 /\
                /  \
               / UI \          Few end-to-end tests
              /------\
             /        \
            /Integration\     Focused integration tests
           /------------\
          /              \
         /   Unit tests   \   Many fast, isolated tests
        /__________________\
  • Unit tests: Usually the largest group. They provide fast feedback about business logic and individual components.
  • Integration tests: Verify important boundaries and interactions, such as API-to-database behavior.
  • End-to-end tests: Validate a smaller set of critical user journeys through the running system.
The pyramid is a guideline, not a fixed percentage rule. The right balance depends on the application, architecture, risks, and cost of maintaining each test type. Avoid duplicating every assertion at every layer.

12. Unit testing in .NET

In .NET, unit tests are commonly written in a separate test project that references the application or class library being tested. A test framework supplies attributes, assertions, test discovery, and lifecycle features.

Example production code

Consider a small pricing service that applies a percentage discount. Keeping the calculation in a class with no database or network dependency makes it straightforward to test.

namespace StoreApp;

public sealed class PriceCalculator
{
    public decimal CalculateFinalPrice(
        decimal price,
        decimal discountPercentage)
    {
        if (price < 0)
            throw new ArgumentOutOfRangeException(nameof(price));

        if (discountPercentage < 0 ||
            discountPercentage > 100)
        {
            throw new ArgumentOutOfRangeException(
                nameof(discountPercentage));
        }

        return price - (price * discountPercentage / 100);
    }
}

Create a test project

For an xUnit-based example, create a test project and add a project reference to the application project. The exact package versions and runner configuration should follow the supported versions selected for your solution.

dotnet new xunit -n StoreApp.Tests
dotnet add StoreApp.Tests reference StoreApp/StoreApp.csproj
dotnet test

Write the unit tests

using StoreApp;
using Xunit;

public class PriceCalculatorTests
{
    [Fact]
    public void CalculateFinalPrice_AppliesDiscount()
    {
        // Arrange
        var calculator = new PriceCalculator();

        // Act
        var result = calculator.CalculateFinalPrice(
            100m, 10m);

        // Assert
        Assert.Equal(90m, result);
    }

    [Theory]
    [InlineData(100, 0, 100)]
    [InlineData(100, 25, 75)]
    [InlineData(200, 50, 100)]
    public void CalculateFinalPrice_ReturnsExpectedValue(
        decimal price,
        decimal discount,
        decimal expected)
    {
        var calculator = new PriceCalculator();

        var result = calculator.CalculateFinalPrice(
            price, discount);

        Assert.Equal(expected, result);
    }

    [Fact]
    public void CalculateFinalPrice_RejectsNegativePrice()
    {
        var calculator = new PriceCalculator();

        Assert.Throws(
            () => calculator.CalculateFinalPrice(-1m, 10m));
    }
}

[Fact] marks a single test in xUnit. [Theory] allows the same test logic to run with multiple sets of input data. Other frameworks provide their own equivalent attributes and data-driven testing features.

13. Arrange, Act, Assert (AAA)

Arrange, Act, Assert is a common structure for writing readable tests.

  1. Arrange: Prepare the objects, dependencies, and input data required by the test.
  2. Act: Execute the method or behavior being tested.
  3. Assert: Verify that the actual outcome matches the expected result.
[Fact]
public void CalculateFinalPrice_AppliesDiscount()
{
    // Arrange
    var calculator = new PriceCalculator();
    decimal price = 250m;
    decimal discount = 20m;

    // Act
    decimal actual = calculator.CalculateFinalPrice(
        price, discount);

    // Assert
    Assert.Equal(200m, actual);
}

AAA is a readability convention, not a requirement that every test must contain exactly three physical code blocks. The important goal is to make setup, execution, and verification easy to identify.

14. .NET testing frameworks and tools

A test framework provides the programming model used to define tests. A test platform is the engine that discovers and runs tests and integrates with development tools and pipelines. These are related but distinct concepts.

Framework / toolTypical useNotes
xUnit.netUnit and integration testing.Open-source framework widely used in .NET projects; supports facts, theories, fixtures, and extensibility.
MSTestUnit and integration testing.Microsoft testing framework with Visual Studio and .NET tooling integration.
NUnitUnit and integration testing.Established .NET testing framework with attributes, parameterized tests, and constraints.
TUnitModern .NET testing.Built around Microsoft.Testing.Platform; check its current compatibility and ecosystem before adopting it.
Microsoft.AspNetCore.Mvc.TestingASP.NET Core integration testing.Provides WebApplicationFactory and test-host support.
Playwright for .NETBrowser automation and UI testing.Automates browser interactions for web applications.
Testcontainers for .NETIntegration testing with real dependencies.Can provision disposable containerized services such as databases for tests.
.NET Aspire testingDistributed application testing.Can start an Aspire AppHost and exercise connected application resources in a realistic test environment.

Test platforms: VSTest and Microsoft.Testing.Platform

In .NET, VSTest and Microsoft.Testing.Platform (MTP) are test platforms. Frameworks such as MSTest, NUnit, and xUnit provide the test-writing APIs and may support one or both platforms depending on their versions and configuration.

Choose a platform deliberately and configure test projects, local developer tooling, and CI pipelines consistently. Do not assume that every framework version or runner configuration supports every platform in the same way.

There is no single framework that is mandatory for every .NET project. Select one based on team familiarity, required integrations, platform support, tooling, and long-term maintenance.

15. Mocks, stubs, fakes and spies

Test doubles are replacement objects used to control or observe dependencies during a test. They help isolate the behavior under test from unrelated systems.

Test doublePurposeExample
DummyPassed to code but not meaningfully used.An unused parameter required by a constructor.
StubReturns predefined responses.A repository stub returns a known customer.
FakeProvides a working but simplified implementation.An in-memory repository used instead of a database.
MockConfigures expected interactions and verifies them.Verify that an email sender was called once.
SpyRecords calls or arguments for later inspection.Capture the notification payload sent by a service.

Example using an interface

public interface IEmailSender
{
    Task SendAsync(
        string recipient,
        string subject,
        CancellationToken cancellationToken);
}

public sealed class RegistrationService
{
    private readonly IEmailSender _emailSender;

    public RegistrationService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public async Task RegisterAsync(
        string email,
        CancellationToken cancellationToken)
    {
        // Registration rules and persistence would run here.

        await _emailSender.SendAsync(
            email,
            "Welcome",
            cancellationToken);
    }
}

A unit test can supply a fake or mock IEmailSender and verify the service's behavior without sending a real email.

Do not mock every dependency automatically. Over-mocking can make tests tightly coupled to implementation details. Prefer testing observable behavior and use real, lightweight implementations when they are practical and reliable.

16. Integration testing in ASP.NET Core

Integration tests check whether multiple application components work together. For an ASP.NET Core API, this can include routing, middleware, model binding, validation, dependency injection, serialization, authentication configuration, and data access.

WebApplicationFactory

WebApplicationFactory<TEntryPoint>, provided by Microsoft.AspNetCore.Mvc.Testing, helps create a test host for an ASP.NET Core application. Tests can send HTTP requests to the in-memory test server through an HttpClient.

using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;

public class ProductsApiTests
    : IClassFixture
{
    private readonly HttpClient _client;

    public ProductsApiTests(
        WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsSuccess()
    {
        // Act
        var response = await _client.GetAsync("/api/products");

        // Assert
        Assert.Equal(
            HttpStatusCode.OK,
            response.StatusCode);
    }
}

The application entry point may need to expose Program to the test assembly, depending on the project structure. Real applications also need deliberate test configuration for databases, authentication, external services, and test data.

What should an API integration test verify?

  • Expected HTTP status code and response content type.
  • Request validation and model-binding behavior.
  • Serialization and deserialization of request and response models.
  • Dependency injection and middleware behavior.
  • Authentication and authorization outcomes.
  • Important persistence and transaction behavior.

17. API testing

API testing evaluates an application's endpoints directly, usually through HTTP requests. It is useful for REST APIs, service-to-service communication, and backend systems consumed by Angular, React, mobile, or other clients.

Important API test areas

AreaWhat to verify
HTTP methodsGET, POST, PUT, PATCH, and DELETE follow the intended resource behavior.
Status codesSuccess, validation, authentication, authorization, not-found, and server-error responses are appropriate.
Request validationRequired fields, data types, ranges, formats, and invalid payloads are handled correctly.
Response contractField names, types, nullability, structure, and error formats remain consistent.
SecurityAuthentication, authorization, input handling, and sensitive-data exposure are checked.
Pagination and filteringPage boundaries, sort order, filters, and invalid query parameters behave as expected.
IdempotencyOperations that promise idempotent behavior remain safe when repeated.
ConcurrencyConflicting updates and optimistic concurrency rules are handled correctly.

Example API test matrix

  • GET an existing product: expect a successful response and the correct product.
  • GET a missing product: expect the API's documented not-found response.
  • POST a valid product: expect creation and a usable resource identifier or location.
  • POST an invalid product: expect validation errors and no unintended persistence.
  • PUT a product: verify the complete replacement behavior defined by the API.
  • PATCH a product: verify only the requested fields change according to the patch contract.
  • DELETE a product: verify deletion behavior and the response contract.
  • Call a protected endpoint without credentials: verify access is rejected.

Tools such as Postman, command-line HTTP clients, and automated .NET test projects can all be used for API testing. Choose automation for important repeatable checks and retain exploratory testing for broader investigation.

18. Database testing

Database testing verifies that data access and database behavior meet the application's requirements. For a .NET application using SQL Server, this may involve EF Core, Dapper, stored procedures, constraints, transactions, and migrations.

What to test

  • Correct insert, read, update, and delete behavior.
  • Primary-key, foreign-key, unique, check, and not-null constraints.
  • Stored procedure inputs, outputs, result sets, and error behavior.
  • Transaction commit and rollback behavior.
  • Concurrency and duplicate-submission scenarios.
  • EF Core mappings, query filters, relationships, and migrations.
  • Data integrity when multiple application operations run together.

In-memory database vs real database

An in-memory provider can be useful for certain isolated tests, but it may not reproduce the behavior of the production database engine. Differences can exist in SQL translation, constraints, transactions, collation, provider-specific functions, and query execution.

For important SQL Server integration behavior, use a dedicated test database or a disposable SQL Server instance, often provisioned through containers when the development and CI environments support it.

Never point automated tests at production data. Use isolated test databases, controlled test records, least-privilege credentials, and cleanup strategies. Tests that modify shared data can become unreliable and can cause real damage.

19. End-to-end and UI testing

End-to-end (E2E) testing validates a complete workflow across the system from the user's perspective. A browser-based test may interact with the frontend, call backend APIs, and persist data through the application.

Example workflow

  1. Open the application and navigate to the sign-in page.
  2. Enter valid credentials and sign in.
  3. Open the product management screen.
  4. Create a product using valid information.
  5. Verify that the product appears in the list.
  6. Edit the product and verify the updated values.
  7. Delete the product and verify the expected final state.

Playwright for .NET can automate browsers and is useful for testing web user journeys. Stable selectors, isolated test data, reliable waits, and clear cleanup are important for maintainable browser tests.

Keep E2E tests focused on critical journeys. A large suite of UI tests can be slower and more fragile than tests at lower levels. Test business rules primarily at the unit or service level where possible.

20. Performance and load testing

Performance testing evaluates responsiveness, throughput, resource use, and stability under a defined workload. It should be based on measurable expectations rather than a vague goal that the application should be fast.

Test typePurpose
Load testingMeasure behavior under expected user traffic or transaction volume.
Stress testingPush the system beyond expected capacity to understand failure behavior.
Spike testingEvaluate sudden increases or decreases in workload.
Soak / endurance testingRun sustained workloads to identify resource leaks or degradation over time.
Volume testingEvaluate behavior with large data volumes.
Scalability testingMeasure how performance changes as resources or application instances are added.

Useful performance measurements

  • Response time: Time taken to complete a request.
  • Throughput: Number of requests or transactions completed in a time interval.
  • Latency percentiles: Values such as p50, p95, and p99 help describe the distribution of response times.
  • Error rate: Percentage or count of failed requests.
  • Resource utilization: CPU, memory, network, database connections, and other resource consumption.
  • Concurrency: Number of operations active at the same time.

For ASP.NET Core, investigate application telemetry, database query performance, connection-pool behavior, thread-pool pressure, external dependency latency, and caching. Use representative data and a controlled environment when benchmarking.

21. Security testing

Security testing evaluates whether an application protects its data, users, and operations against misuse and attack. Security should be considered throughout development, not only before release.

Common security test areas

  • Authentication: Verify identity checks, token validation, session handling, and credential protections.
  • Authorization: Confirm users can access only the resources and operations permitted by their roles, claims, or policies.
  • Input validation: Test malformed and unexpected inputs, including injection attempts.
  • Data exposure: Check that responses, logs, and error messages do not reveal secrets or sensitive information.
  • Transport security: Verify HTTPS and secure handling of communication.
  • Dependency security: Scan packages and address known vulnerable dependencies.
  • Rate limiting and abuse resistance: Test controls that protect sensitive or expensive endpoints.
  • Configuration: Check secrets management, debug settings, CORS policies, and production configuration.

For .NET applications, combine code review, static analysis, dependency scanning, automated tests, and appropriate dynamic security testing. Follow current security guidance for the frameworks and services you deploy.

22. Smoke, sanity and regression testing

TypeMeaningExample
Smoke testingA broad, shallow check that a build or deployment is stable enough for further testing.Application starts, health endpoint responds, and sign-in page loads.
Sanity testingA focused check that a specific change or fix behaves as expected.Verify the corrected discount calculation and closely related behavior.
Regression testingChecks that existing behavior has not been unintentionally broken by a change.Run order, payment, and reporting tests after modifying shared pricing logic.
RetestingRepeats a failed test after a defect fix to confirm the specific issue is resolved.Re-run the exact steps that previously produced an incorrect result.

Teams sometimes use “sanity” differently, so define the term in the project's test process rather than relying on the label alone.

23. Test-Driven Development (TDD) and Behavior-Driven Development (BDD)

Test-Driven Development

TDD is a development practice in which a developer writes a test for a small behavior before implementing that behavior. The common cycle is:

  1. Red: Write a test that expresses the expected behavior and confirm that it fails for the intended reason.
  2. Green: Implement the simplest code that makes the test pass.
  3. Refactor: Improve the design while keeping the tests passing.

TDD can help clarify requirements and encourage small, testable units. It is a practice, not a guarantee of good design or complete test coverage.

Behavior-Driven Development

BDD encourages collaboration around examples of expected behavior, often using language that product owners, developers, and testers can understand.

Feature: Apply a discount

Scenario: Customer receives a valid discount
  Given the order subtotal is 100
  And the discount rate is 10 percent
  When the final price is calculated
  Then the final price should be 90

BDD scenarios can be automated with suitable tools, but the main value is the shared understanding of behavior. Avoid writing scenarios that merely repeat implementation details.

24. Code coverage and test quality

Code coverage measures which parts of the code were exercised while tests ran. It can help identify code that has not been reached by the current test suite.

Common coverage measures

  • Line coverage: Percentage of executable lines visited by tests.
  • Statement coverage: Percentage of executable statements run.
  • Branch coverage: Percentage of decision outcomes exercised.
  • Method coverage: Percentage of methods invoked during tests.

High coverage does not automatically mean high test quality. A test may execute a line without asserting the correct behavior. Meaningful assertions, relevant boundary cases, and realistic failure scenarios matter more than chasing a single coverage percentage.

What makes a good automated test?

  • It verifies an observable behavior or requirement.
  • It is deterministic and repeatable.
  • It has a clear name and useful failure message.
  • It is independent of unrelated test execution order.
  • It uses controlled data and cleans up resources.
  • It runs at the appropriate test level.
  • It avoids unnecessary dependence on implementation details.

25. Test data and environment management

Reliable tests need predictable data and environments. A test that depends on a developer's local database, current time, network state, or shared records may pass on one machine and fail on another.

Test data practices

  • Use isolated records with known identifiers or unique test values.
  • Create the data required by a test rather than depending on records left by another test.
  • Clean up created data or reset the environment between test runs.
  • Use synthetic or anonymized data instead of exposing real personal information.
  • Control time, randomness, and external responses when those values affect the test.
  • Keep credentials and secrets outside source control.

Environment types

EnvironmentPurpose
Local developmentDeveloper workstation used for coding and quick tests.
Automated testControlled environment for repeatable unit and integration tests.
QA / testShared environment for broader functional and exploratory testing.
Staging / pre-productionProduction-like environment used for release validation.
ProductionLive environment serving real users and business workloads.

26. Testing in CI/CD pipelines

Continuous Integration (CI) runs automated checks as code changes are committed or proposed. Continuous Delivery or Deployment (CD) extends the process toward packaging and releasing validated software.

Typical pipeline flow

Developer pushes code
        |
        v
Restore dependencies
        |
        v
Build solution
        |
        v
Run unit tests
        |
        v
Run integration tests
        |
        v
Static analysis / security checks
        |
        v
Publish test results and artifacts
        |
        v
Deploy to staging
        |
        v
Smoke / acceptance checks
        |
        v
Release approval or deployment

Not every test belongs in every pipeline stage. Fast checks can run on each pull request, while longer performance, browser, or environment-dependent suites may run on a schedule or before a release.

Run tests from the .NET CLI

# Restore dependencies
dotnet restore

# Build the solution
dotnet build --no-restore

# Run tests
dotnet test --no-build

# Run a specific test project
dotnet test tests/StoreApp.Tests/StoreApp.Tests.csproj

# Collect coverage when the required collector is configured
dotnet test --collect:"XPlat Code Coverage"

Configure the selected test platform and coverage collector consistently with the framework and SDK versions in use. Publish test results in the CI system so failures are visible and traceable.

27. Modern testing practices

Containerized integration tests

Testcontainers for .NET can start disposable dependencies such as databases or message brokers in containers. This can make integration tests closer to production behavior while keeping test infrastructure isolated. Container runtime availability and CI permissions must be considered.

Distributed application testing with .NET Aspire

For solutions built with .NET Aspire, the testing packages can launch the AppHost and its configured resources for functional or integration testing across service boundaries. This is useful when a test needs to verify that multiple applications and dependencies work together rather than testing a single component in isolation.

Contract testing

Contract testing checks that a service provider and its consumers agree on an interface, message, or API contract. It is particularly useful when independently developed services need to evolve without unexpected compatibility breaks.

Mutation testing

Mutation testing introduces small changes to production code—such as changing a comparison or removing a statement—and checks whether the test suite detects them. Surviving mutations can reveal weak assertions or missing test cases. Mutation testing adds execution cost, so use it selectively.

Property-based testing

Property-based testing generates many input values and verifies general properties that should always hold. For example, a normalization function might be expected to produce the same result when applied twice. This can uncover edge cases that hand-picked examples miss.

Observability-assisted testing

Logs, metrics, traces, and health checks can help diagnose failures in integration and distributed-system tests. Assertions should still focus on expected behavior; telemetry helps explain why a test failed.

AI-assisted testing

AI tools can help brainstorm edge cases, draft test data, explain failures, or suggest test scenarios. Treat generated tests as proposals: review the assertions, validate assumptions, protect sensitive code and data, and run the tests against the actual application. AI-generated tests can reproduce the same misunderstanding as the prompt that produced them.

28. Practical .NET testing workflow

Consider a typical full-stack application with an Angular frontend, ASP.NET Core Web API, and SQL Server database. A practical testing approach separates checks by responsibility while verifying the important interactions.

Suggested solution structure

MyApplication.sln
|
+-- src/
|   +-- MyApplication.Api/
|   +-- MyApplication.Application/
|   +-- MyApplication.Domain/
|   +-- MyApplication.Infrastructure/
|
+-- tests/
    +-- MyApplication.Domain.Tests/
    +-- MyApplication.Application.Tests/
    +-- MyApplication.Api.IntegrationTests/
    +-- MyApplication.Architecture.Tests/

This is an example structure, not a mandatory convention. Small applications may need fewer projects, while larger systems may organize tests by feature or bounded context.

What belongs at each level?

Test projectExample responsibilities
Domain testsEntity rules, value objects, domain services, and calculations.
Application testsUse-case orchestration, validation, authorization decisions, and dependency interactions.
API integration testsRoutes, middleware, HTTP contracts, dependency injection, serialization, and persistence integration.
Architecture testsSelected dependency rules and structural constraints, when these are important to the solution.

Example feature: create an order

  1. Unit tests: Verify quantity rules, price calculations, discount limits, and order-state transitions.
  2. Application tests: Verify that the use case validates the request and coordinates the required services.
  3. Integration tests: Verify that the API accepts a valid request, persists the order, and returns the documented response.
  4. Database tests: Verify constraints, relationships, transaction behavior, and important SQL queries.
  5. End-to-end tests: Verify that a user can create and view an order through the frontend.
  6. Performance tests: Measure order creation under a defined workload if it is a critical or high-volume workflow.

This layered approach avoids trying to prove every detail through slow UI tests. It also helps teams locate failures more quickly.

29. Common testing mistakes

  • Testing only the happy path: Include invalid inputs, boundaries, missing data, and failure conditions.
  • Writing tests that assert nothing meaningful: A test should verify the expected outcome, not merely execute a method.
  • Overusing mocks: Excessive mocks can test interactions that do not represent real user-visible behavior.
  • Using shared mutable test data: Tests can interfere with one another and become order-dependent.
  • Relying only on in-memory database behavior: Provider differences can hide SQL-specific issues.
  • Making UI tests cover every rule: Keep detailed business-rule checks at faster, lower levels where practical.
  • Ignoring flaky tests: Intermittent failures reduce trust in the test suite and should be investigated.
  • Chasing coverage alone: Coverage is a signal, not proof of correctness.
  • Ignoring test maintenance: Update tests when requirements change and remove obsolete checks.
  • Running tests against production: Use isolated environments and controlled data.
  • Not testing authorization: Verify both allowed and denied access paths for protected resources.

30. Developer testing checklist

Use this checklist when implementing or reviewing a feature.

  • Understand the requirement and acceptance criteria.
  • Identify normal, invalid, boundary, and failure scenarios.
  • Write unit tests for important business logic.
  • Test validation and error-handling behavior.
  • Test integration points such as database and external services.
  • Verify API status codes and response contracts.
  • Test authentication and authorization rules.
  • Use isolated test data and a safe test environment.
  • Run the relevant regression tests.
  • Review test failures and flaky behavior.
  • Run automated checks in the CI pipeline.
  • Record unresolved risks before release.

31. Software testing interview questions

1. What is software testing?

It is the process of evaluating software to find defects and determine whether its behavior meets specified requirements and intended needs.

2. What is the difference between verification and validation?

Verification checks whether the product is being built according to specifications. Validation checks whether the product meets user needs and intended use.

3. What is the difference between unit and integration testing?

Unit testing checks an isolated unit of behavior. Integration testing checks whether multiple components or systems work together correctly.

4. What is regression testing?

Regression testing checks that existing behavior has not been unintentionally broken by a code, configuration, or environment change.

5. What is the Arrange, Act, Assert pattern?

Arrange prepares the test, Act executes the behavior, and Assert verifies the outcome.

6. What is mocking?

Mocking uses a test double to configure and verify interactions with a dependency, helping isolate the behavior being tested.

7. What is code coverage?

Code coverage measures which parts of source code are exercised during test execution. It does not by itself measure whether assertions are meaningful.

8. What is the test pyramid?

It is a strategy model that generally favors many fast unit tests, a smaller number of integration tests, and fewer end-to-end tests.

9. How do you test an ASP.NET Core Web API?

Use unit tests for isolated business logic and integration tests—often with WebApplicationFactory—to exercise the HTTP pipeline, routing, middleware, dependency injection, and configured dependencies.

10. Why might an in-memory database test be insufficient?

An in-memory provider may not reproduce the production database engine's SQL translation, constraints, transactions, collation, or provider-specific behavior.

11. What is the difference between a test framework and a test platform?

A framework provides the APIs and attributes used to write tests. A platform discovers and runs tests and integrates with tools and CI systems.

12. What is flaky testing?

A flaky test sometimes passes and sometimes fails without a relevant code change, often because of timing, shared state, external dependencies, or nondeterministic data.

32. Further learning and official resources

Use official documentation for current setup instructions, supported framework versions, and detailed examples. Tooling evolves, so check the documentation that matches the SDK and package versions used by your project.