Uniting the Pieces: Exploring the Synergy Between Unit and Integration Testing in Software Development.

Uniting the Pieces: Exploring the Synergy Between Unit and Integration Testing in Software Development.

We have reached the seventh episode of our journey into the world of software testing. The main topic of this article will be "Unit Test" and "Integration Test".

Unit testing is a crucial aspect of software development, focusing on testing individual units of code in isolation from the rest of the system. Usually conducted by the developers, its primary objectives are to find faults within these units and ensure their correct functional behavior. This process includes both white box and black box testing techniques and often requires significant effort, although automation can alleviate some of it. Despite its importance, unit testing should not become a religious debate but rather a practical and integral part of the development process (1.).

When conducting unit tests, developers focus on several key aspects of the code, including interface testing, evaluating boundary conditions, exploring independent paths, and scrutinizing error-handling paths.

Let's break the ice and start with a simple unit test example.

I wrote this example in java and I used JUnit as the unit testing framework.

In this example we have a ShoppingCart class that represents a shopping cart containing a list of Product objects.

import java.util.ArrayList;
import java.util.List;

public class ShoppingCart {
    private List<Product> products = new ArrayList<>();

    public void addProduct(Product product) {
        products.add(product);
    }

    public void removeProduct(Product product) {
        products.remove(product);
    }

    public boolean containsProduct(Product product) {
        return products.contains(product);
    }

    public int getProductCount() {
        return products.size();
    }

    public double calculateTotalPrice() {
        double totalPrice = 0.0;
        for (Product product : products) {
            totalPrice += product.getPrice();
        }
        return totalPrice;
    }
}

public class Product {
    private String id;
    private String name;
    private double price;

    public Product(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}        

I create a test class ShoppingCartTest with three test methods:

testAddProduct, testRemoveProduct, and testCalculateTotalPrice.

import org.junit.Test;
import static org.junit.Assert.*;

public class ShoppingCartTest {

    @Test
    public void testAddProduct() {
        // Arrange
        ShoppingCart cart = new ShoppingCart();
        Product product = new Product("123", "Laptop", 999.99);

        // Act
        cart.addProduct(product);

        // Assert
        assertTrue(cart.containsProduct(product));
        assertEquals(1, cart.getProductCount());
    }

    @Test
    public void testRemoveProduct() {
        // Arrange
        ShoppingCart cart = new ShoppingCart();
        Product product = new Product("123", "Laptop", 999.99);
        cart.addProduct(product);

        // Act
        cart.removeProduct(product);

        // Assert
        assertFalse(cart.containsProduct(product));
        assertEquals(0, cart.getProductCount());
    }

    @Test
    public void testCalculateTotalPrice() {
        // Arrange
        ShoppingCart cart = new ShoppingCart();
        Product product1 = new Product("123", "Laptop", 999.99);
        Product product2 = new Product("456", "Mouse", 29.99);
        cart.addProduct(product1);
        cart.addProduct(product2);

        // Act
        double totalPrice = cart.calculateTotalPrice();

        // Assert
        assertEquals(1029.98, totalPrice, 0.01);
    }
}        


Each test method follows the Arrange-Act-Assert pattern: it sets up the initial state of the system under test (Arrange), performs the action being tested (Act), and then verifies the expected outcome (Assert) (2.).

I used various assertions provided by JUnit, such as assertTrue, assertFalse, and assertEquals, to verify the correctness of the tested behavior.

Below is the output of the execution of the UnitTest in the "IntelliJ IDEA" IDE, as we can see as output we have not only the result of the tests but also the information on the coverage of the classes being tested (3.).

IntelliJ IDEA Community Edition: "ShoppingCartTest" execution output.


This example shows us that to test a class by itself (ShoppingCart), we need a “Driver” (ShoppingCartTest), that is, another class that calls the class under test and passes it appropriate test input values.

Generally speaking, a Driver is a simplified implementation of a class that invokes the methods of another class or component. Drivers are used to provide inputs to the unit under test and simulate its external behavior.

We will soon see that drivers are developed during the Bottom-Up approach of integration testing as they can be used to test lower levels of code, when the higher level of code or modules is not developed.


But how can we test a module in isolation if this module depends on other modules not yet developed? The answer is "with the Stubs”.

A Stub is a simplified implementation of a class or interface that provides predetermined responses to method calls. Stubs are used in scenarios where the unit being tested depends on other modules or components that are not yet available or fully implemented. They simulate the behavior of these external dependencies to enable the testing of the unit in isolation. Here are four important considerations on the role of the stub.

  1. A Stub can effectively replace a function. Each test case should correspond to a different instance of the stub (or a different branch of the stub).
  2. A Stub does not effectively replace a class. It cannot manage the state (attribute values) of a class between two stub calls, and it cannot handle the testing of a sequence of interactions.
  3. A stub can be written based on a 'black box' knowledge of the module to be emulated.
  4. A stub is developed during the Top-Down approach of integration testing as it can be used to test higher levels of code, when the lower level of code or modules is not developed (we will soon see this).


Unit Testing: Driver module, Stubs Module and Test cases.


Let's continue with an example of Stub in Java.

Let’s suppose to have a WeatherService interface with a method getTemperature that fetches the current temperature from an external API. We want to test a WeatherReporter class that depends on this interface.

public interface WeatherService {
  int getTemperature(String city);
}

public class WeatherReporter {
  private WeatherService weatherService;

  public WeatherReporter(WeatherService weatherService) {
      this.weatherService = weatherService;
  }
    
  public String reportWeather(String city) {
      int temperature = weatherService.getTemperature(city);
      return "Current temperature in "+ city +" is "+ temperature + "°C.";
  }
}        

Below we see WeatherReporterTest which is the driver module to perform the Unit Test of WeatherReporter.

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class WeatherReporterTest {
  @Test
  public void testReportWeather() {

      // Stub implementation of WeatherService
      WeatherService weatherServiceStub = new WeatherService() {
        @Override
        public int getTemperature(String city) {
          return 25; // Simulate temperature for testing
        }
      };

      WeatherReporter reporter = new WeatherReporter(weatherServiceStub);

      assertEquals("Current temperature in London is 25°C.",            reporter.reportWeather("London"));
    }
}        

In this example, WeatherServiceStub is a Stub implementation of the WeatherService interface. It provides a predetermined temperature value (25°C) when the getTemperature method is called. This allows us to test the WeatherReporter class without relying on the actual external weather service.


Once all the units have been tested, it is necessary to proceed with integration testing to evaluate whether they work correctly as a whole. This phase is called Integration “Integration Test(1.), (2.).

Integration testing consists of considering increasingly larger sets of modules, until the complete set is obtained.

Integration testing is a very critical phase of testing, where individual units are combined and tested as a group. This can be done incrementally or all at once, each approach with its advantages and disadvantages. Incremental integration, although more time-consuming, offers greater control and helps to avoid chaos during full integration.

The dependency graph, which we have already seen in detail in the previous article on cyclomatic complexity and test coverage (.3), is very useful in this phase. It can be obtained from the analysis of the application's source code or even with automatic tools.

Several strategies guide the testing and integration process, including top-down and bottom-up strategies.

The top-down strategy is a method in which integration testing takes place from top to bottom following the control flow of software system. The higher level modules are tested first and then lower level modules are tested and integrated in order to check the software functionality. Stubs are used for testing if some modules are not ready. It starts with the main program and stubs, gradually replacing stubs with modules:

  • Start with Main and Stubs.
  • Replace stubs incrementally, we can choose to traverse the graph breadth-first or depth-first.
  • Test each integration.

The advantage is that the program gradually takes shape. It helps ensure the big picture is correct. It is therefore possible to obtain a first prototype soon.

The disadvantage is that it requires to write many stubs.


Top Down Strategy: Diagrammatic Representation.


The bottom-up strategy is a strategy in which the lower level modules are tested first. These tested modules are then further used to facilitate the testing of higher level modules. The process continues until all modules at top level are tested. This strategy begins with drivers and leaves modules, progressively replacing upper drivers with modules:

  • Start? with driver and leaves module.
  • Replace upper drivers with modules.
  • Test each integration.

The advantage is that drivers are often easier to write than stubs (but not always), and can be derived from unit test code (quite often).

The disadvantage is that?program doesn't work until integration is complete, so an early prototype is not possible.

Another disadvantage is that critical modules (at the top level of software architecture) which control the flow of application are tested last and may be prone to defects.

Bottom-Up Strategy: Diagrammatic Representation


But if the dependency graph has a circular dependency, what should we do? Well, the right strategy would be to eliminate the circular dependency from the code and bring it back to a layered structure.

Believe me, it's really the thing that needs to be done, don't carry around technical debts that one day you'll have to pay later with high interest.

However, let's see a technique to solve the integration test even in this situation:

  • Begin by opening the loops using stubs.
  • For instance, in the case of a loop A->B->C->A,
  • Duplicate the module A to create A-Stub.
  • Integrate the modules as A->B->C->A-Stub (non-cyclic).
  • Replace A with A-Stub, testing the entire loop.


Solving loops technique: Open loops using stubs.


I insist, even at the risk of being repetitive, do not use techniques like the one described above to manage integration testing. This approach does not solve the underlying problem. The solution must be radical. The cyclic dependency in the code needs to be eliminated.


Another integration strategy is the sandwich model strategy. The sandwich model is an integration testing strategy that combines elements of both top-down and bottom-up approaches. In this model, integration testing begins with testing at both the top and bottom levels of the software architecture simultaneously, gradually moving towards the center.

The sandwich model offers flexibility in choosing the integration order of modules, allowing developers to tailor the approach based on specific project needs and development priorities.


The last integration strategy I want to mention is the Feature Set strategy.

The Feature Set in integration testing strategy refers to an approach where related modules or components that collectively implement a specific feature or functionality are integrated and tested together. Instead of integrating and testing individual modules or components in isolation, the feature set approach focuses on testing groups of modules or components that work together to deliver a particular feature or set of features.



The Feature Set Approach offers several advantages, including reduced overhead in terms of stubs or drivers creation, as only related modules are integrated and tested together.

One potential disadvantage of it is that it may be challenging to isolate and test individual features within a larger cluster, especially if features have complex dependencies or interactions.




Let's see yet another example of unit test, this time we consider a more real situation where the class under test depends on other classes.

In fact to build an object oriented system we will inevitably need to make different objects interact with one another.

In this example we will see how to how to create unit test for classes that have external dependencies.

The class that I'm going to test is LoginUseCaseSync.

First, let me explain the meaning of this class name: LoginUseCaseSync.

"UseCase" means that this object is responsible for execution of a function of flow. The suffix "Sync" states that the flow executed by this use case is long-running and will block the calling thread.

To perform a logging flow this use case requires three additional objects:

  • LoginHttpEndpointSync is an abstraction of networking layer and should be used to execute network requests.
  • AuthTokenCache is an abstraction of persistence mechanism and should be used to store user authentication token obtained from the server.
  • EvenBusPoster is an abstraction of events notification mechanism and should be used to broadcast events that happen in the system.

There is just one single public method in this class, loginSync. It takes user name and password as arguments.

The return value of this method is UseCaseResult enum. The value of this enum will tell the caller whether the login flow completed successfully.

It can be either success, failure, or network error. The caller can use this information to either proceed with application flow or perform error handling.

There is also one private method in this class isSuccessfulEndpointResult but we don't need to care about that at all. It's an unit internal implementation detail and we should not see it when testing them.

So, this is the class I'd like to test.

public class LoginUseCaseSync {

  private final LoginHttpEndpointSync mLoginHttpEndpointSync;
  private final AuthTokenCache mAuthTokenCache;
  private final EventBusPoster mEventBusPoster;

  public LoginUseCaseSync(LoginHttpEndpointSync loginHttpEndpointSync,
                        AuthTokenCache authTokenCache,
                        EventBusPoster eventBusPoster) {
    mLoginHttpEndpointSync = loginHttpEndpointSync;
    mAuthTokenCache = authTokenCache;
    mEventBusPoster = eventBusPoster;
  }

  public UseCaseResult loginSync(String username, String password) {
    LoginHttpEndpointSync.EndpointResult endpointEndpointResult;
      try {
            endpointEndpointResult = mLoginHttpEndpointSync.
                loginSync(username, password);
      } catch (NetworkErrorException e) {
        return UseCaseResult.NETWORK_ERROR;
      }

      if (isSuccessfulEndpointResult(
        endpointEndpointResult)) {
        mAuthTokenCache.cacheAuthToken(
            endpointEndpointResult.getAuthToken());
        mEventBusPoster.postEvent(new LogEvent());
        return UseCaseResult.SUCCESS;
      } else {
        return UseCaseResult.FAILURE;
      }
  }

  private boolean isSuccessfulEndpointResult(
      LoginHttpEndpointSync.EndpointResult endpointResult) {
    return endpointResult.getStatus() ==  LoginHttpEndpointSync.EndpointResultStatus.SUCCESS;
  }
}

public enum UseCaseResult {
    SUCCESS,
    FAILURE,
    NETWORK_ERROR
}        

Following are the other classes on which LoginUseCaseSynk depends:

public interface LoginHttpEndpointSync {
  /**
   * Log in using provided credentials
   * @return the aggregated result of login operation
   * @throws NetworkErrorException if login attempt failed 
   * due to network error
   */
  EndpointResult loginSync(String username, String password) 
      throws NetworkErrorException;

  enum EndpointResultStatus {
    SUCCESS,
    AUTH_ERROR,
    SERVER_ERROR,
    GENERAL_ERROR
  }

  class EndpointResult {
    private final EndpointResultStatus mStatus;
    private final String mAuthToken;

    public EndpointResult(EndpointResultStatus status, String authToken) {
      mStatus = status;
      mAuthToken = authToken;
    }

    public EndpointResultStatus getStatus() {
      return mStatus;
    }

    public String getAuthToken() {
      return mAuthToken;
    }
  }
}

public class NetworkErrorException 
		extends Exception {
}        
public interface EventBusPoster {
    void postEvent(Object event);
}

public class LogEvent {
}        
public interface AuthTokenCache {
    void cacheAuthToken(String authToken);
    String getAuthToken();
}        


I create a test class as usual LoginUseCaseSyncTest and I define the system under test SUT.

To instantiate LoginUseCaseSync (SUT) I need to pass the required dependencies into its constructor. However, I can't use the real implementations here.

For example, if I pass the real LoginHttpEndpointSync into my system under test, then it will try to execute real network requests during the tests. Not only it's slow, but I want to run the unit tests and get reliable results regardless of whether I'm connected to the Internet or not. The same goes for AuthTokenCache and EventBusPoster.

If I use the real implementations of these objects it will make my unit slow and unreliable. For unit tests to be useful, we must be 100 percent confident that if they pass then we can ship the unit into production and if it's not then the problems are confined within one specific unit.

Unreliable unit tests can be extremely harmful.

Therefore using real external dependencies in unit test is not possible in many cases.

Well, I can substitute the other units with alternative implementations that I write specifically to be used in tests. These alternative implementations are called the "stubs".

The difference between the real unit and the stub is that the stub's internal implementation is optimized for tests.

For example, the real unit AuthTokenCache can persist data into on-disk database while stub simply returns a predefined data.

For example, if the real unit LoginHttpEndpointSync executes network requests and returns data from the server, it can be substituted with a stub that can be programmed to return the data that is needed for tests.

Before writing the stubs, let's decide which test cases we want to implement. This will guide us in constructing the stubs appropriately.

Here is the list of test cases we want:

  • username and password passed to the endpoint
  • if login succedes - user's auth token must be cached
  • if login fails - login event posted to event bus
  • if login succedes - login event posted to event bus
  • if login fails - no login event posted
  • if login succedes - success returned
  • fails - fail returned
  • network - network error returned


With the stubs made as shown below we can create the test cases we have established.

public class LoginUseCaseSyncTest {
  public static final String USERNAME = "username";
  public static final String PASSWORD = "password";
  public static final String AUTH_TOKEN = "authToken";
  public static final String NON_INITIALIZED_AUTH_TOKEN = "noAuthToken";

  LoginHttpEndpointSyncStub mLoginHttpEndpointSyncStub;
  AuthTokenCacheStub mAuthTokenCacheStub;
  EventBusPosterStub mEventBusPosterStub;

  LoginUseCaseSync SUT;

  @Before
  public void setup() throws Exception {
    mLoginHttpEndpointSyncStub = new LoginHttpEndpointSyncStub();
    mAuthTokenCacheStub = new AuthTokenCacheStub();
    mEventBusPosterStub = new EventBusPosterStub();
    SUT = new LoginUseCaseSync(mLoginHttpEndpointSyncStub, mAuthTokenCacheStub, mEventBusPosterStub);
  }

  // ---------------------------------------------------------------------------------------------
  // Stubs classes

  private static class LoginHttpEndpointSyncStub implements LoginHttpEndpointSync {
    public String mUsername = "";
    private String mPassword = "";
    public boolean mIsGeneralError;
    public boolean mIsAuthError;
    public boolean mIsServerError;
    public boolean mIsNetworkError;

    @Override
    public EndpointResult loginSync(String username, String password) throws NetworkErrorException {
      mUsername = username;
      mPassword = password;
      if (mIsGeneralError) {
        return new EndpointResult(EndpointResultStatus.GENERAL_ERROR, "");
      } else if (mIsAuthError) {
        return new EndpointResult(EndpointResultStatus.AUTH_ERROR, "");
      }  else if (mIsServerError) {
        return new EndpointResult(EndpointResultStatus.SERVER_ERROR, "");
      } else if (mIsNetworkError) {
        throw new NetworkErrorException();
      } else {
        return new EndpointResult(EndpointResultStatus.SUCCESS, AUTH_TOKEN);
      }
    }
  }

  private static class AuthTokenCacheStub implements AuthTokenCache {
    String mAuthToken = NON_INITIALIZED_AUTH_TOKEN;

    @Override
    public void cacheAuthToken(String authToken) {
            mAuthToken = authToken;
        }

    @Override
    public String getAuthToken() {
            return mAuthToken;
        }
  }

  private static class EventBusPosterStub implements EventBusPoster {
    public Object mEvent;
    public int mInteractionsCount;

    @Override
    public void postEvent(Object event) {
      mInteractionsCount++;
      mEvent = event;
    }
  }

// test cases are in the next code snippet
}        


Here it is the code that creates the list of test cases that we previously decided for our unit test.

//  this code is inside the class LoginUseCaseSyncTest 

@Test
  public void loginSync_success_usernameAndPasswordPassedToEndpoint() 
        throws Exception {
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mLoginHttpEndpointSyncStub.mUsername, is(USERNAME));
    assertThat(mLoginHttpEndpointSyncStub.mPassword, is(PASSWORD));
  }

  @Test
  public void loginSync_success_authTokenCached() 
      throws Exception {
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mAuthTokenCacheStub.getAuthToken(), is(AUTH_TOKEN));
  }

  @Test
  public void loginSync_generalError_authTokenNotCached() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsGeneralError = true;
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mAuthTokenCacheStub.getAuthToken(), is(NON_INITIALIZED_AUTH_TOKEN));
  }

  @Test
  public void loginSync_authError_authTokenNotCached() throws Exception {
    mLoginHttpEndpointSyncStub.mIsAuthError = true;
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mAuthTokenCacheStub.getAuthToken(), is(NON_INITIALIZED_AUTH_TOKEN));
  }

  @Test
  public void loginSync_serverError_authTokenNotCached() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsServerError = true;
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mAuthTokenCacheStub.getAuthToken(), is(NON_INITIALIZED_AUTH_TOKEN));
  }

  @Test
  public void loginSync_success_loggedInEventPosted() 
      throws Exception {
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mEventBusPosterStub.mEvent, is(instanceOf(LogEvent.class)));
  }

  @Test
  public void loginSync_generalError_noInteractionWithEventBusPoster() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsGeneralError = true;
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mEventBusPosterStub.mInteractionsCount, is(0));
  }

  @Test
  public void loginSync_authError_noInteractionWithEventBusPoster() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsAuthError = true;
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mEventBusPosterStub.mInteractionsCount, is(0));
  }

  @Test
  public void loginSync_serverError_noInteractionWithEventBusPoster() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsServerError = true;
    SUT.loginSync(USERNAME, PASSWORD);
    assertThat(mEventBusPosterStub.mInteractionsCount, is(0));
  }

  @Test
  public void loginSync_success_successReturned() 
      throws Exception {
    UseCaseResult result = SUT.loginSync(USERNAME, PASSWORD);
    assertThat(result, is(UseCaseResult.SUCCESS));
  }

  @Test
  public void loginSync_serverError_failureReturned() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsServerError = true;
    UseCaseResult result = SUT.loginSync(USERNAME, PASSWORD);
    assertThat(result, is(UseCaseResult.FAILURE));
  }

  @Test
  public void loginSync_authError_failureReturned() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsAuthError = true;
    UseCaseResult result = SUT.loginSync(USERNAME, PASSWORD);
    assertThat(result, is(UseCaseResult.FAILURE));
  }

  @Test
  public void loginSync_generalError_failureReturned() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsGeneralError = true;
    UseCaseResult result = SUT.loginSync(USERNAME, PASSWORD);
    assertThat(result, is(UseCaseResult.FAILURE));
  }

  @Test
  public void loginSync_networkError_networkErrorReturned() 
      throws Exception {
    mLoginHttpEndpointSyncStub.mIsNetworkError = true;
    UseCaseResult result = SUT.loginSync(USERNAME, PASSWORD);
    assertThat(result, is(UseCaseResult.NETWORK_ERROR));
  }        


And this is the output of our unit test where we can see that the tests have all passed and where we can also see the information relating to the test coverage.


IntelliJ IDEA Community Edition: "LoginUseCaseSyncTest " execution output.




The seventh episode of the journey into the realm of software testing is over, summing up we can say that:

Unit testing is an indispensable practice in modern software development, offering numerous benefits that contribute to the overall quality, reliability, and maintainability of software systems. Unit tests play a crucial role in refactoring and continuous integration by providing a reliable means to verify code changes and ensure software stability.

I want to underline three aspects in software development that benefit greatly from having a large suite of unit tests, here they are:

  • Facilitating Refactoring and Code Changes: Unit tests provide a safety net for developers when refactoring or making changes to existing code. By having a comprehensive suite of unit tests in place, developers can refactor code with confidence, knowing that any regressions or unintended side effects will be quickly identified by failing unit tests. This enables teams to evolve and improve their codebase over time without introducing new bugs or breaking existing functionality.
  • Encouraging Modularity and Code Reusability: Unit testing encourages the development of modular, reusable, and loosely coupled code components. Writing unit tests for individual units of code encourages developers to design their software in a modular fashion, where each component performs a specific task and can be tested independently. This modular approach not only improves code maintainability and readability but also fosters code reusability across different projects or modules.
  • Enabling Continuous Integration and Delivery: Unit testing plays a crucial role in enabling continuous integration (CI) and continuous delivery (CD) practices in software development. By automating the execution of unit tests as part of the CI/CD pipeline, development teams can quickly identify and address issues introduced by new code changes. This automated testing process ensures that the software remains in a deployable state at all times, facilitating frequent releases and faster time-to-market.

One final observation, as I already said at the beginning of this article the unit testing, despite its importance, should not become a religious debate but rather an integrated part of the development process. I am quite sceptical about software development methodologies such as TDD (Test Driven Development) where the unit tests are writing before the class to be tested.

One of the next articles will focus on TDD


That's all about Unit and Integration Testing.

See you on the next episode.


I remind you my newsletter "Sw Design & Clean Architecture"? : https://lnkd.in/eUzYBuEX where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.

Thanks for reading my article, and I hope you have found the topic useful,

Feel free to leave any feedback.

Your feedback is very appreciated.

Thanks again.

Stefano


References:

1. Vladimir Khorikov, "Unit Testing Principles, Practices, and Patterns?", Manning Pubblication (2020).

2. Gerard Meszaros, “xUnit Test Patterns, Refactoring Test Code”, Addison-Wesley (2007)

3. S.Santilli: "https://www.dhirubhai.net/pulse/power-white-box-testing-code-paths-coverage-stefano-santilli-0wuif "

要查看或添加评论,请登录

社区洞察

其他会员也浏览了