Skip to main content
Lesson 2

Creating a Test Method

Back to T280: Testing Business Logic

Lesson 2: Creating a Test Method

While the purpose of a test project is to contain tests for a customization library, the purpose of a test class is to contain tests for a graph or graph extension, the purpose of a test method is to contain tests of some functionality implemented in the graph or graph extension being tested. The topics of this chapter describe how to create a method that contains unit tests for a graph, how to register the necessary services, and how to manage a single test or a group of tests.

Test Method: General Information

In a test class, which is intended for testing a graph or graph extension, you will create test methods, each of which will test a particular functionality. Through the xUnit.net library, which you will use for creating unit tests in the activities of this guide, you can create test methods without parameters and test methods with parameters. In addition to the presence or absence of parameters, these kinds of methods differ in the attributes they have and the number of unit tests they produce.

Learning Objectives

In this chapter, you will learn how to do the following:

  • Create test methods without parameters
  • Create an instance of the tested graph
  • Create and update objects in the graph cache
  • Verify that the desired conditions are met for the values present in the method
  • Run and debug test methods
  • Create test methods with parameters
  • Set values for the test parameters
  • Create unit tests for graph extensions

Applicable Scenarios

You create a test method if you want to test some business logic of a graph or graph extension.

Development of a Test

A unit test is an automated test written by a soware developer that tests some independent unit of code. A unit test usually launches some functionality of a soware component and then compares the resulting values with the expected values. As you develop unit tests, the best practice is to create them according to the standard AAA pattern:

  1. Arrange: You prepare the test's context and assign some values.
  2. Act: You launch the functionality of a graph or graph extension.
  3. Assert: You check the result.
    With the Acumatica Unit Test Framework, you can easily create unit tests according to this process. In the arrange part, you initialize the context until actions can be correctly executed. To use the Acumatica Unit Test Framework for this part, you may need to first enable some application features (by using the Common.UnitTestAccessProvider.EnableFeature generic method) or initialize Lesson 2: Creating a Test Method | 14
    the required setup DACs (by using the Setup generic method). Aer that, you create an instance of a graph (by using the PXGraph.CreateInstance generic method) and fill its caches. Some automated tests, typically referred to as integration tests, validate the interaction of multiple independent components. With Acumatica Unit Test Framework, you can implement these tests and check the interaction of multiple components within a graph.

Creation of Test Methods Without Parameters

You create a test method without parameters as follows:

  • You declare a method as public void.
  • You assign the Fact attribute to the test method. This produces one test for the test method.

Creation of Test Methods with Parameters

When you create a test method with parameters, you declare it as public void and assign it the needed parameters. You assign the Theory attribute to the test method and add as many InlineData attributes as the method has parameters. This produces one test for each set of parameters provided in the InlineData attribute. In each InlineData attribute, you specify constants separated by commas as the values for the method parameters. The number of the constants and their types must match the number and the types of the test method parameters. You can provide values for the method parameters in the ClassData or MemberData attributes instead of the InlineData attributes.

Testing of the Business Logic of a Graph

In the test method, you create an instance of the tested graph by calling the PXGraph.CreateInstance<> generic method. In this generic method, you use the graph class as the type parameter. You create data access class (DAC) records in the graph cache (PXCache objects). You can address a specific cache of a graph either by the DAC name (.Cachestypeof(<DAC_name>)) or by the graph view that contains the DAC objects (..Cache).

Creation of a Test Method for a Graph Extension

In a test method for a graph extension that contains logic for a DAC extension, you get the DAC extension object with the GetExtension<> generic method. You alter the DAC extension object as you wish, and then you update the whole DAC object along with its extension in the cache by using the PXCache.Update method. This executes the logic implemented in the graph extension so that you can verify its correctness.

Assertions

You can use the methods of the Xunit.Assert class to make assertions in unit tests. For example, the Assert.True static method verifies that its argument is equal to True, and the Assert.False static method verifies that its argument is equal to False. The following table lists additional methods of the Xunit.Assert class that may be useful. Lesson 2: Creating a Test Method | 15

Table: Assert Class Methods

Method Description

False(bool condition) Verifies that condition is false. The method also throws a FalseException if condition is not false.

True(bool condition) Verifies that condition is true and throws a True- Exception if condition is not true.

Contains(T expected, IEnumerable Verifies that a collection contains a given object. The collection) method also throws a ContainsException if the collection does not contain the object.

DoesNotContain(T expected, IEnumer- Verifies that a collection does not contain a given able collection) object. The method also throws a DoesNotCon- tainException if the collection contains the ob- ject.

Empty(IEnumerable collection) Verifies that collection is empty and throws an EmptyException if collection is not empty.

NotEmpty(IEnumerable collection) Verifies that collection is not empty and throws a NotEmptyException if collection is empty.

Single(IEnumerable collection) Verifies that collection contains a single element. The method also throws a SingleException if collection does not contain exactly one element.

Null(object? obj) Verifies that the object reference is null and throws a NullException if the object reference is not null.

NotNull(object? obj) Verifies that an object reference is not null, and throws a NotNullException if the object reference is null.

Contains(string expectedSubstring, Verifies that actualString contains expect- string? actualString) edSubstring by using the current culture. The method also throws a ContainsException if ac- tualString does not contain expectedSub- string.

DoesNotContain(string expectedSub- Verifies that actualString does not contain ex- string, string? actualString) pectedSubstring by using the current culture. The method also throws a DoesNotContainExcep- tion if actualString contains expectedSub- string.

StartsWith(string? expectedS- Verifies that actualString starts with expect- tartString, string? actualString) edStartString by using the current culture. The method also throws a StartsWithException if actualString does not start with expectedS- tartString. Lesson 2: Creating a Test Method | 16

Method                                                      Description

EndsWith(string? expectedEndString,                         Verifies that actualString ends with expect-
string? actualString)                                       edEndString by using the current culture, and
                                                            throws an EndsWithException if actualString
                                                            does not end with expectedEndString.

Equal(string? expected, string? actu-                       Verifies that the actual and expected strings are
al)                                                         equivalent. The method also throws an EqualEx-
                                                            ception if the actual and expected strings are
                                                            not equivalent.

Equal<T>(T expected, T actual)                              Verifies that two objects are equal by using a default
                                                            comparer, and throws an EqualException if the
                                                            objects are not equal.

Equal<T>(IEnumerable<T> expected,                           Verifies that two sequences of the unmanaged type T
IEnumerable<T> actual)                                      are equal. The method also throws an EqualExcep-
                                                            tion if the sequences are not equal.

You can use another assertion library instead of the Xunit.Assert class.

Running and Debugging of Test Methods

Visual Studio includes the Test Explorer tool, which you can use to run a test, a test method, a group of test methods, or all available test methods. You can also debug a single test or multiple tests.

Activity 2.1: To Create a Test Method Without Parameters

The following activity will walk you through the process of creating a test method without parameters.

Story

Suppose that you want to make sure that the following behavior of the Repair Services (RS201000) custom form has not changed: The selection of the Walk-In Service check box and the selection of the Requires Preliminary Check check box are mutually exclusive. You need to create a test method that changes the state of one check box and checks the state of the other check box.

Process Overview

To create the test method, you will create a public void method and assign it the Fact attribute. In the method, you will create an instance of the tested graph. In the cache of the graph, you will create a record. In this record, you will change the values of the fields that indicate the states of the check boxes, update the cache of the graph, and check that the values of dependent fields have been changed according to the tested logic of the graph.

Step: Creating a Test Method Without Parameters To create a method for checking the states of the check boxes of the Repair Services (RS201000) form, do the following:

  1. In the RSSVRepairServiceMaintTests class, use the following code to create a public void method and name it PreliminaryCheckAndWalkInServiceFlags_AreOpposite. Lesson 2: Creating a Test Method | 17
           public void PreliminaryCheckAndWalkInServiceFlags_AreOpposite()
           {
           }
    
  2. By using the following code, assign the Fact attribute to the method to specify that this unit test is called without parameters.
           [Fact]
           public void PreliminaryCheckAndWalkInServiceFlags_AreOpposite()
    
  3. In the PreliminaryCheckAndWalkInServiceFlags_AreOpposite method, create an instance of the RSSVRepairServiceMaint graph as follows.
                var graph = PXGraph.CreateInstance<RSSVRepairServiceMaint>();
    
  4. Now that the RSSVRepairServiceMaint graph is initialized, use the following code to create an RSSVRepairService object that represents a record in the table of the Repair Services form.
                var repairService =
                    graph.Caches[typeof(RSSVRepairService)].
                    Insert(new RSSVRepairService
                    {
                        ServiceCD = "Service1",
                        Description = "Service 1",
                        WalkInService = true,
                        PreliminaryCheck = false
                    }) as RSSVRepairService;
    

    Objects that represent table records are created in the graph cache (PXCache objects). You can address a specific cache of a graph either by the DAC name (graph.Cachestypeof(<DAC_name>)) or by the graph view that contains the DAC objects (graph..Cache).
  5. Aer a record is created, determine whether a change of the Walk-In Service check box state leads to a change of the Requires Preliminary Check check box state. Also, determine whether a change of the Requires Preliminary Check check box state leads to a change of the Walk-In Service check box. Perform this checking by adding the following code.
                if (repairService != null)
                {
                    repairService.WalkInService = false;
                    graph.Caches[typeof(RSSVRepairService)].Update(repairService);
                    Assert.True(repairService.PreliminaryCheck);
    
                     repairService.WalkInService = true;
                     graph.Caches[typeof(RSSVRepairService)].Update(repairService);
                     Assert.False(repairService.PreliminaryCheck);
    
                     repairService.PreliminaryCheck = false;
                     graph.Caches[typeof(RSSVRepairService)].Update(repairService);
                     Assert.True(repairService.WalkInService);
    
                     repairService.PreliminaryCheck = true;
                     graph.Caches[typeof(RSSVRepairService)].Update(repairService);
                     Assert.False(repairService.WalkInService);
                }
    

Lesson 2: Creating a Test Method | 18

Test Method: Test Management in Visual Studio

The test methods that you have created appear in the Test Explorer window of Visual Studio, which is shown in the following screenshot. For each test method without parameters (see Activity 2.1: To Create a Test Method Without Parameters), a test is added. For parameterized test methods (see Activity 2.4: To Create a Test Method with Parameters), a test is added for each set of parameter values that you have specified for the method. In the Test Explorer window of Visual Studio, you can select the desired test, test method, or group of test methods, and you can run (see Activity 2.2: To Run a Test Method) or debug (see Activity 2.3: To Debug a Test Method) them. You can also get information about the tests' outcomes (whether they have succeeded or not) and execution time.

Figure: The Test Explorer window with added tests

Activity 2.2: To Run a Test Method

The following activity will walk you through the process of running a single test method.

Process Overview

In the Test Explorer window of Visual Studio, you will select a method and run it.

Step: Running a Test Method

          If your code contains test methods other than the one created in Activity 2.1: To Create a Test Method
          Without Parameters, or if you have already run the existing test method before, the number of items
          mentioned in the following instruction may differ.

Do the following to run the method created in Activity 2.1: To Create a Test Method Without Parameters:

  1. In Visual Studio, select the Test > Test Explorer menu item. The Test Explorer window opens, which currently displays no tests. Lesson 2: Creating a Test Method | 19
  2. Click Run All Tests In View. The solution is built, and one test for the PreliminaryCheckAndWalkInServiceFlags_AreOpposite test method appears in the Test Explorer window (shown in the following screenshot) and is run.
       Figure: The Test Explorer window with one test
    

Activity 2.3: To Debug a Test Method

The following activity will walk you through the process of debugging a test method.

Process Overview

In Visual Studio, you will set breakpoints both in the code being tested and in the test method, launch the debugging for the test method from the Test Explorer window, and check that the values of the fields are as expected.

Step: Debugging a Test Method To debug the PreliminaryCheckAndWalkInServiceFlags_AreOpposite method, do the following:

  1. In the PhoneRepairShop_Code.Tests project, open the RSSVRepairServiceMaintTests.cs file.
  2. Move the caret to the following line.
        repairService.WalkInService = false;
    
  3. Press F9 to set a breakpoint on the line.
  4. In the PhoneRepairShop_Code project, open the RSSVRepairServiceMaint.cs file.
  5. In the _(Events.FieldUpdated<RSSVRepairService, RSSVRepairService.walkInService> e) method, move the caret to the following line.
        row.PreliminaryCheck = !(row.WalkInService == true);
    
  6. Press F9 to set a breakpoint on the line.
  7. Select the Test > Test Explorer menu command to open the Test Explorer window.
  8. Right-click the PreliminaryCheckAndWalkInServiceFlags_AreOpposite method, and select Debug. Aer the debugging process starts, the execution initially Lesson 2: Creating a Test Method | 20
        pauses at the breakpoint in the RSSVRepairServiceMaint.cs file. This is because the
        _(Events.FieldUpdated<RSSVRepairService, RSSVRepairService.walkInService>
        e) method gets triggered when the repairService object is first created and added to the cache
        using the graph.Caches[typeof(RSSVRepairService)].Insert(...) method of the graph.
        Continue debugging (you can press F5 for this).
    
  9. The execution pauses in the RSSVRepairServiceMaintTests.cs file. You can verify that the value of repairService.WalkInService is true. 10.Step over the statement in the current line (you can use F10 for this). The value of repairService.WalkInService becomes false. 11.Step over the statement in the current line. This updates the repairService object in the cache. During the update, the FieldUpdated event is generated for the Walk-In Service check box, and the _(Events.FieldUpdated<RSSVRepairService, RSSVRepairService.walkInService> e) method of the RSSVRepairServiceMaint class is launched. The execution pauses at the breakpoint in the RSSVRepairServiceMaint.cs file, which signifies that the extension library and unit test match each other. 12.Continue debugging (you can press F5 for this). The debugging process continues until it completes successfully.

Activity 2.4: To Create a Test Method with Parameters

The following activity will walk you through the process of creating a test method with parameters.

Story

Suppose that you want to make sure that the following behavior of the customized Stock Items (IN202500) form has not changed: The selection of the Repair Item check box causes the Repair Item Type drop-down list to be available, and the clearing of the Repair Item check box causes the Repair Item Type drop-down list to be unavailable. You need to create a test method that selects or clears the Repair Item check box and checks whether the Repair Item Type drop-down list is available or unavailable, respectively.

Process Overview

To create the test method, you will create a public void method with one Boolean parameter. You will assign the method the Theory attribute and two InlineData attributes, each of which has the possible values of the method parameter (true and false). In the method, you will create an instance of the tested graph. In the cache of the graph, you will create a record. For this record, by using the GetExtension generic method, you will retrieve the extension, which contains custom fields. Depending on the method parameter, you will change the state of the check box, update the cache of the graph, and make sure that the availability of the drop-down list has been changed according to the tested logic of the graph.

System Preparation

Before you begin creating the test method, create the InventoryItemMaintTests test class. For an example that shows how to create a test class, see Activity 1.2: To Create a Test Class.

Step 1: Creating and Configuring a Test Method with Parameters To create and configure a method for testing the business logic of the InventoryItemMaint class, do the following:

  1. In the InventoryItemMaintTests class, create a public void method, and name it RepairItemTypeEnabled_WhenRepairItemSelected. Lesson 2: Creating a Test Method | 21
  2. To specify that this unit test is called with parameters, assign the Theory attribute to the method as follows.
                 [Theory]
                 public void RepairItemTypeEnabled_WhenRepairItemSelected
    
  3. Add the Boolean enabled parameter to the RepairItemTypeEnabled_WhenRepairItemSelected method as follows.
                 public void RepairItemTypeEnabled_WhenRepairItemSelected
                     (bool enabled)
    
  4. Add two InlineData attributes, which provide two values for the enabled parameter, as follows.
                 [Theory]
                 [InlineData(true)]
                 [InlineData(false)]
                 public void RepairItemTypeEnabled_WhenRepairItemSelected
                     (bool enabled)
    
      The RepairItemTypeEnabled_WhenRepairItemSelected method will be called twice: The first
      call will pass true as its argument, and the second will pass false.
    

Step 2: Implementing a Method for Testing the InventoryItemMaint Class To test the business logic of the InventoryItemMaint class, do the following:

  1. In the RepairItemTypeEnabled_WhenRepairItemSelected method, create an instance of the InventoryItemMaint graph by adding the following code.
                      var graph = PXGraph.CreateInstance<InventoryItemMaint>();
    
  2. Aer the InventoryItemMaint graph is initialized, create an InventoryItem object as follows.
                      InventoryItem item =
                          (InventoryItem)graph.Caches[typeof(InventoryItem)].Insert(
                              new InventoryItem
                              {
                                  InventoryCD = "Item1",
                                  Descr = "Item 1"
                              });
    
  3. Get the InventoryItemExt extension of the InventoryItem object as follows.
                      InventoryItemExt itemExt = item.GetExtension<InventoryItemExt>();
    
  4. Select (or clear) the Repair Item check box by using the needed value of the enabled parameter, and make sure that the Repair Item Type drop-down list is available (or, respectively, unavailable) by adding the following code.
                      itemExt.UsrRepairItem = enabled;
                      graph.Caches[typeof(InventoryItem)].Update(item);
                      PXFieldState fieldState =
                          ((PXFieldState)graph.Caches[typeof(InventoryItem)].GetStateExt<
                          InventoryItemExt.usrRepairItemType>(item));
                      Assert.True(enabled == fieldState.Enabled);
    

Lesson 2: Creating a Test Method | 22

                 A call of the PXCache.Update method for the InventoryItem object updates both the
                 object and its extension.

As a result of the actions you have performed in this activity, the InventoryItemMaintTests.cs file contains the following code.

using Xunit;
using PX.Data;
using PX.Tests.Unit;
using PX.Objects.IN;
using PhoneRepairShop;

namespace PhoneRepairShop_Code.Tests
{
    public class InventoryItemMaintTests : TestBase
    {
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void RepairItemTypeEnabled_WhenRepairItemSelected
            (bool enabled)
        {
            var graph = PXGraph.CreateInstance<InventoryItemMaint>();

               InventoryItem item =
                   (InventoryItem)graph.Caches[typeof(InventoryItem)].Insert(
                       new InventoryItem
                       {
                           InventoryCD = "Item1",
                           Descr = "Item 1"
                       });

               InventoryItemExt itemExt = item.GetExtension<InventoryItemExt>();

               itemExt.UsrRepairItem = enabled;
               graph.Caches[typeof(InventoryItem)].Update(item);
               PXFieldState fieldState =
                   ((PXFieldState)graph.Caches[typeof(InventoryItem)].GetStateExt<
                   InventoryItemExt.usrRepairItemType>(item));
               Assert.True(enabled == fieldState.Enabled);
          }
     }
}


          This test uses the arrange-act-assert pattern.

In some circumstances, running this test method may fail. To learn the actions that you must perform to make this test method successful, see Activity 2.5: To Register a Service.

Test Method: Registration of Services

Acumatica uses the dependency injection technique to design soware. With this technique, an object (a client) receives other objects (services) that it depends on; in this context, these services are called dependencies. Each Lesson 2: Creating a Test Method | 23

of these services must be registered: For each needed interface, you must specify the service that implements this interface. This soware design pattern is used to decouple code components and make them easier to extend and test. For more information about dependency injection, see https://docs.microsoft.com/en-us/dotnet/core/extensions/ dependency-injection.

Implementation of the Dependency Injection in Acumatica ERP and Unit Tests

In Acumatica ERP, the Autofac library is used to implement the dependency injection. For more information, see Dependency Injection.

In unit tests, mock services are used instead of real complex objects. These mock services are simplified services that mimic real ones. To register a service, you need to override the TestBase.RegisterServices method and make calls to the Autofac.ContainerBuilder.RegisterType generic method. The following table includes the frequently used mock services that are available in Acumatica ERP.

Table: Frequently Used Mock Services

 Mocked Interface                                           Mock Service

 IFinPeriodRepository (defined in the PX.Ob-                FinPeriodServiceMock (defined in the PX.Ob-
 jects.GL.FinPeriods namespace)                             jects.Unit namespace)

 IPXCurrencyService (defined in the PX.Ob-                  CurrencyServiceMock (defined in the PX.Ob-
 jects.CM.Extensions namespace)                             jects.Unit namespace)

You can also define your own service, register it, and use it in a PX.Tests.Unit.TestBase-derived class as described in Dependency Injection.

Activity 2.5: To Register a Service

The following activity will walk you through the process of registering a mock service in a test class.

Story

Suppose that running a test method leads to the generation of the Autofac.Core.Registration.ComponentNotRegisteredException exception. When this exception occurs, the error message specifies which component (interface) is not registered. You need to register the service that implements the specified interface.

Process Overview

To get rid of the exception, you will add the use of the Autofac library and override the TestBase.RegisterServices method in the test class. In the overridden RegisterServices method, you will register the needed service that implements the interface specified in the exception message.

Step: Registering a Service When the test method created in Activity 2.4: To Create a Test Method with Parameters is run, the following error message is generated if a proper service is not registered (only part of the error message is quoted). Lesson 2: Creating a Test Method | 24

Autofac.Core.Registration.ComponentNotRegisteredException : The requested service 'System.Func`2[[PX.Data.PXGraph, PX.Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3b136cac2f602b8e], PX.Objects.CM.Extensions.IPXCurrencyService, PX.Objects, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency. To register this service, do the following:

  1. In the PhoneRepairShop_Code.Tests.csproj project, add a reference to Autofac.dll from the Bin folder of the Acumatica ERP instance, as described in Activity 1.1: To Create a Test Project.
  2. Add the following using directives to the InventoryItemMaintTests.cs file.
      using Autofac;
      using PX.Objects.CM.Extensions;
    
  3. In the InventoryItemMaintTests class, override the TestBase.RegisterServices method as follows.
                protected override void RegisterServices(ContainerBuilder builder)
                {
                    base.RegisterServices(builder);
                    builder.RegisterType<PX.Objects.Unit.CurrencyServiceMock>().
                        As<IPXCurrencyService>();
                }
    

Aer the service is registered, run the tests for the RepairItemTypeEnabled_WhenRepairItemSelected test method, which had failed previously. Make sure that the tests succeed. Lesson 3: Correctly Assigning Field Values in Tests | 25