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.
Learning roadmap
- Software testing fundamentals
- Why testing matters
- Verification and validation
- Testing principles
- Software testing lifecycle
- Testing levels
- Functional and non-functional testing
- Manual and automated testing
- Test cases and test scenarios
- Test design techniques
- Test pyramid and test strategy
- Unit testing in .NET
- Arrange, Act, Assert
- .NET testing frameworks
- Mocks, stubs, fakes and spies
- Integration testing in ASP.NET Core
- API testing
- Database testing
- End-to-end and UI testing
- Performance and load testing
- Security testing
- Smoke, sanity and regression testing
- TDD and BDD
- Code coverage and test quality
- Test data and environment management
- Testing in CI/CD pipelines
- Modern testing practices
- Practical .NET testing workflow
- Common testing mistakes
- Developer testing checklist
- Interview questions
- Further learning and official resources
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
| Term | Meaning |
|---|---|
| Test | An execution or evaluation used to check a particular behavior or quality attribute. |
| Test scenario | A high-level situation or workflow that should be evaluated. |
| Test case | A defined set of preconditions, inputs, actions, and expected results. |
| Test data | Input values and supporting records used during testing. |
| Expected result | The behavior or output that should occur according to the requirement. |
| Actual result | The behavior or output observed when the test is performed. |
| Defect / bug | A flaw that can cause the software to behave incorrectly or fail to meet a requirement. |
| Regression | An unintended change in existing behavior after code or configuration is modified. |
| Test suite | A 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.
3. Verification and validation
Verification and validation are related but different quality activities.
| Verification | Validation |
|---|---|
| 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
- Testing shows the presence of defects: Tests can reveal defects, but passing tests do not prove that defects are absent.
- Exhaustive testing is impractical: Testing every possible input and state is usually impossible, so testing must be prioritized.
- Test early: Starting quality activities early can reduce the cost and impact of fixing defects.
- Defects cluster: A small number of components often account for a large share of observed defects. Use defect history and risk to guide attention.
- Tests can become stale: Repeating the same tests may stop finding new defects. Review and improve test cases as the product changes.
- Testing is context-dependent: A medical system, a payment service, and a content website have different risks and quality requirements.
- 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.
- Requirement analysis: Understand user stories, acceptance criteria, business rules, dependencies, and risks.
- Test planning: Define scope, approach, responsibilities, environments, tools, schedule, and completion criteria.
- Test design: Create test scenarios, test cases, data sets, and expected results.
- Environment preparation: Configure application builds, databases, credentials, services, and test data.
- Test execution: Run manual and automated tests and record actual results.
- Defect reporting: Document reproducible failures with evidence, steps, environment, and impact.
- Retesting and regression: Verify fixes and check that related existing behavior still works.
- 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.
| Level | Focus | .NET example |
|---|---|---|
| Unit | One small unit of behavior in isolation. | Test a C# pricing or validation service without a real database. |
| Integration | Interaction between components or systems. | Test an ASP.NET Core endpoint with dependency injection and a test database. |
| System | The behavior of the complete integrated application. | Test a deployed application across API, database, authentication, and other services. |
| Acceptance | Whether 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.
| Type | What it evaluates |
|---|---|
| Performance | Response time, throughput, resource usage, and behavior under load. |
| Load | Behavior under an expected or specified workload. |
| Stress | Behavior beyond normal capacity and how the system fails or recovers. |
| Scalability | Ability to handle growth in users, data, or workload as resources change. |
| Security | Resistance to unauthorized access, data exposure, and common vulnerabilities. |
| Usability | How effectively and comfortably users can operate the application. |
| Accessibility | Whether people with different abilities can use the product. |
| Compatibility | Behavior across supported browsers, devices, operating systems, and environments. |
| Reliability and recovery | Consistency 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 testing | Automated 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 case | Input / condition | Expected result |
|---|---|---|
| Valid login | Registered username and correct password. | User is authenticated and receives the expected destination or response. |
| Incorrect password | Registered username and incorrect password. | Authentication is rejected without exposing sensitive details. |
| Unknown user | Username that is not registered. | Authentication is rejected with an appropriately generic response. |
| Missing username | Username is empty. | Input validation reports the required field. |
| Missing password | Password is empty. | Input validation reports the required field. |
| Locked or disabled account | Account 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.
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.
- Arrange: Prepare the objects, dependencies, and input data required by the test.
- Act: Execute the method or behavior being tested.
- 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 / tool | Typical use | Notes |
|---|---|---|
| xUnit.net | Unit and integration testing. | Open-source framework widely used in .NET projects; supports facts, theories, fixtures, and extensibility. |
| MSTest | Unit and integration testing. | Microsoft testing framework with Visual Studio and .NET tooling integration. |
| NUnit | Unit and integration testing. | Established .NET testing framework with attributes, parameterized tests, and constraints. |
| TUnit | Modern .NET testing. | Built around Microsoft.Testing.Platform; check its current compatibility and ecosystem before adopting it. |
| Microsoft.AspNetCore.Mvc.Testing | ASP.NET Core integration testing. | Provides WebApplicationFactory and test-host support. |
| Playwright for .NET | Browser automation and UI testing. | Automates browser interactions for web applications. |
| Testcontainers for .NET | Integration testing with real dependencies. | Can provision disposable containerized services such as databases for tests. |
| .NET Aspire testing | Distributed 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.
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 double | Purpose | Example |
|---|---|---|
| Dummy | Passed to code but not meaningfully used. | An unused parameter required by a constructor. |
| Stub | Returns predefined responses. | A repository stub returns a known customer. |
| Fake | Provides a working but simplified implementation. | An in-memory repository used instead of a database. |
| Mock | Configures expected interactions and verifies them. | Verify that an email sender was called once. |
| Spy | Records 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.
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
| Area | What to verify |
|---|---|
| HTTP methods | GET, POST, PUT, PATCH, and DELETE follow the intended resource behavior. |
| Status codes | Success, validation, authentication, authorization, not-found, and server-error responses are appropriate. |
| Request validation | Required fields, data types, ranges, formats, and invalid payloads are handled correctly. |
| Response contract | Field names, types, nullability, structure, and error formats remain consistent. |
| Security | Authentication, authorization, input handling, and sensitive-data exposure are checked. |
| Pagination and filtering | Page boundaries, sort order, filters, and invalid query parameters behave as expected. |
| Idempotency | Operations that promise idempotent behavior remain safe when repeated. |
| Concurrency | Conflicting 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.
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
- Open the application and navigate to the sign-in page.
- Enter valid credentials and sign in.
- Open the product management screen.
- Create a product using valid information.
- Verify that the product appears in the list.
- Edit the product and verify the updated values.
- 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.
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 type | Purpose |
|---|---|
| Load testing | Measure behavior under expected user traffic or transaction volume. |
| Stress testing | Push the system beyond expected capacity to understand failure behavior. |
| Spike testing | Evaluate sudden increases or decreases in workload. |
| Soak / endurance testing | Run sustained workloads to identify resource leaks or degradation over time. |
| Volume testing | Evaluate behavior with large data volumes. |
| Scalability testing | Measure 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
| Type | Meaning | Example |
|---|---|---|
| Smoke testing | A 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 testing | A focused check that a specific change or fix behaves as expected. | Verify the corrected discount calculation and closely related behavior. |
| Regression testing | Checks that existing behavior has not been unintentionally broken by a change. | Run order, payment, and reporting tests after modifying shared pricing logic. |
| Retesting | Repeats 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:
- Red: Write a test that expresses the expected behavior and confirm that it fails for the intended reason.
- Green: Implement the simplest code that makes the test pass.
- 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
| Environment | Purpose |
|---|---|
| Local development | Developer workstation used for coding and quick tests. |
| Automated test | Controlled environment for repeatable unit and integration tests. |
| QA / test | Shared environment for broader functional and exploratory testing. |
| Staging / pre-production | Production-like environment used for release validation. |
| Production | Live 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 project | Example responsibilities |
|---|---|
| Domain tests | Entity rules, value objects, domain services, and calculations. |
| Application tests | Use-case orchestration, validation, authorization decisions, and dependency interactions. |
| API integration tests | Routes, middleware, HTTP contracts, dependency injection, serialization, and persistence integration. |
| Architecture tests | Selected dependency rules and structural constraints, when these are important to the solution. |
Example feature: create an order
- Unit tests: Verify quantity rules, price calculations, discount limits, and order-state transitions.
- Application tests: Verify that the use case validates the request and coordinates the required services.
- Integration tests: Verify that the API accepts a valid request, persists the order, and returns the documented response.
- Database tests: Verify constraints, relationships, transaction behavior, and important SQL queries.
- End-to-end tests: Verify that a user can create and view an order through the frontend.
- 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.
- Testing in .NET – Microsoft Learn
- Unit testing best practices with .NET – Microsoft Learn
- Integration tests in ASP.NET Core – Microsoft Learn
- .NET test platforms overview – Microsoft Learn
- .NET Aspire testing overview – Microsoft Learn
- Playwright for .NET documentation
- Testcontainers for .NET documentation