How To Unit Test Soorderentry Extension In Acumatica

 

Hello everybody,

today I want to show to unit test, and I mean really unit test SOOrderEntry graph extnesion in Acumatica with XUnit.

In order to achieve it, you'll need following steps:

  1. Create .Net Class library
  2. Reference xUnit
  3. Create public class that inherits from TestBase class
  4. Add override ResisterServices 
  5. Create something like PrepareGraph with usage of TestBase.Setup class
  6. Write your mehtods.

I will not describe how steps 1 - 4 may look, as it is pretty obvious, but step 5 and 6 at C# level may look like this:

public class SOOrderEntrySDExtTests : TestBase
{
    protected IPXCurrencyService CurrencyService;
    protected IFinPeriodRepository FinPeriodService;
 
    public SOOrderEntryExtTests()
    {
        FinPeriodService = new PX.Objects.Unit.FinPeriodServiceMock();
        CurrencyService = new PX.Objects.Unit.CurrencyServiceMock();
    }
 
    protected override void RegisterServices(ContainerBuilder builder)
    {
        base.RegisterServices(builder);
        builder
            .Register<Func<PXGraphIFinPeriodRepository>>(context
                =>
            {
                return (graph)
                    =>
                {
                    return FinPeriodService;
                };
            });
        builder
            .Register<Func<PXGraphIPXCurrencyService>>(context
                =>
            {
                return (graph)
                    =>
                {
                    return CurrencyService;
                };
            });
    }
 
    private SOOrderEntry PrepareGraph()
    {
        Setup<SOOrderEntry>(
           new SOSetup
           {
               DefaultOrderType = "SC",
               TransferOrderType = "TR",
               ShipmentNumberingID = "SOSHIPMENT",
               ProrateDiscounts = true,
               FreeItemShipping = "S",
               FreightAllocation = "A",
               CreditCheckError = false,
               MinGrossProfitValidation = "W"
           });
        
        
        var graph = PXGraph.CreateInstance<SOOrderEntry>();
        graph.CurrentDocument.Insert(
            new SOOrder()
            {
                OrderType = "SC",
                OrderDesc = "some test desc"
            }
            );
        var graphExtension = graph.GetExtension<SOOrderEntryExt>();
        
 
        return graph;
    }
 
    [Fact]
    public void CheckInsertion()
    {
        var graph = PrepareGraph();
        graph.CurrentDocument.Current.Approved = true;
        graph.CurrentDocument.Update(graph.CurrentDocument.Current);
 
        Assert.Equal(graph.CurrentDocument.Current.CuryID, "USD");
 
    }
}

Few more comments regarding code. 

  1. As of now, in order to give to SOOrderEntry setup classes, you'll need to go from one exception message to another. Not very convenient, but the only available way
  2. RegisterServices was copy/pasted from Github post of Dmitriy Naumov
  3. You may need to take a look on graphExtnesion
  4. Regarding extension fields I suggest to set up them in the cache

Summary

Acumatica team put big amount of efforts in order to add stability to their products. Also they put a lot of efforts for giving line for adding stability to Acumatica by other ISV. It will be a crime not to benefit from it. Crime against customers!

 

 

 

Unit Test Acumatica Pxgraph

 

Hello everybody,

today I want to make another description of how to cover with Unit Tests Graphs in Acumatica. 

First of all I want to say that Acumatica controllers or as Acumatica names them graphs do not have any way to inject any dependency from interface. In such case it can be useful idea to use Microsoft Fakes library. 

So in order to unit test Acumatica code you will have a need to make mix of Acumatica code with Microsoft shims. Imagine that you have following graph declaration:

public class PAllocs : PXGraph<PaymentsAllocation>

and somewhere in your graph you have method like this:

        [PXButton(Tooltip = "Saves values from grid")]
        [PXUIField(DisplayName = "Save")]
        public virtual IEnumerable Save1(PXAdapter adapter)
        {
            var inputtedPoOrders = this.VendorOrders.Select()
            .ToList().Where(a => a.GetItem<POOrder>().GetExtension<POOrderExt>().AllocatedAmount > 0
            ||
                                    a.GetItem<POOrder>().GetExtension<POOrderExt>().AllocatedAmount < 0
            ).ToList();
 
            string currentDif = this.ReportItemFilter.Current.DifAmt.ToString();
 
            var apPayment = this.ReleasedPayments.Current;
            
            if (apPayment.CuryOrigDocAmt != ReportItemFilter.Current.TargetAmt)
            {
                VendorFilter.Ask("""Choose another item or modify po ords in bottom grid",
                    "Please choose another check or modify Amounts in bottom grid",
                    MessageButtons.OK, MessageIcon.Error);
                return adapter.Get();
            }
 
 
            if (inputtedPoOrders.Count == 0 || currentDif != "0.0000")
            {
                var vfRow = this.VendorFilter.Current;
                this.VendorFilter.Ask(vfRow, "Input at least one row""You should enter at least one Allocation payment, and Diff should be 0.0000",
                    MessageButtons.OK, MessageIcon.Error);
                return adapter.Get();
            }
        }

how to unit test it with Microsoft fakes?

Preparation

As I already mentioned in one of my articles, you'll need to prepare your test solution for creation of needed graphs. First step will be to make app.config in your test solution.

Below goes app.config that you'll need to add to your solution with tests. 

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionGroup name="system.web" type="System.Web.Configuration.SystemWebSectionGroup, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
      <section name="pxaccess" type="PX.Data.PXAccessSection, PX.Data" />
      <section name="pxdatabase" type="PX.Data.PXDatabaseSection, PX.Data" />
      <section name="pxtrace" type="PX.Data.PXTraceSection, PX.Data" />
      <section name="pxtranslate" type="PX.Data.PXTranslationSection, PX.Data" />
      <section name="basicAuth" type="PX.Export.Authentication.BasicAuthenticationSection, PX.Export" />
      <section name="formsAuth" type="PX.Export.Authentication.FormsAuthenticationSection, PX.Export" />
      <section name="multiAuth" type="PX.Export.Authentication.AuthenticationManagerSection, PX.Export" />
      <section name="externalAuth" type="PX.Export.Authentication.ExternalAuthenticationSection, PX.Export" />
      <section name="webDAV" type="PX.Export.WebDAV.WebDAVSection, PX.Export" />
      <section name="activeDirectory" type="PX.Data.Access.ActiveDirectorySection, PX.Data" />
      <section name="attachments" type="PX.Data.EP.DynamicAttachmentSection, PX.Data" />
      <section name="odata" type="PX.Api.OData.ODataConfigurationSection, PX.Api.OData" />
      <section name="fullTrustAssemblies" type="System.Web.Configuration.FullTrustAssembliesSection, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" allowDefinition="MachineToApplication" />
    </sectionGroup>
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  </configSections>
  <connectionStrings>
    <remove name="ProjectX" />
    <add name="ProjectX" providerName="System.Data.SqlClient" connectionString="Data Source=DESKTOP-0EC1M8R;Initial Catalog=MohawkDB;Integrated Security=False;User ID=sa;Password=123" />
  </connectionStrings>
  <location inheritInChildApplications="false">
    <system.web>
      <machineKey validationKey="187C46E676C11D108F5AB1CEAB16A4CABFB1DEF8A77B54045FBAE8556E015744C6A5099963847BD36D3B673752455ABBC4528CF24A0F2EE3F1B3AC49397C0F51" decryptionKey="2134B0CA7E59C637F365F74FA10727B6C2BDC2E9D26ADE2A" validation="SHA1" />
      <trust level="Full" originUrl="" />
      <fullTrustAssemblies />
      <pxdatabase defaultProvider="PXSqlDatabaseProvider">
        <providers>
          <remove name="PXSqlDatabaseProvider" />
          <add name="PXSqlDatabaseProvider" type="PX.Data.PXSqlDatabaseProvider, PX.Data" connectionStringName="ProjectX" companyID="" secureCompanyID="False" />
        </providers>
      </pxdatabase>
      <pxaccess defaultProvider="PXDatabaseAccessProvider">
        <providers>
          <remove name="PXDatabaseAccessProvider" />
          <add name="PXDatabaseAccessProvider" type="PX.Data.PXDBFeatureAccessProvider, PX.Data" applicationName="/" administratorRole="Administrator" />
        </providers>
      </pxaccess>
      <membership defaultProvider="PXActiveDirectorySyncMembershipProvider">
        <providers>
          <remove name="PXActiveDirectorySyncMembershipProvider" />
          <remove name="MySQLMembershipProvider" />
          <add name="PXActiveDirectorySyncMembershipProvider" type="PX.Data.PXActiveDirectorySyncMembershipProvider, PX.Data" mainProviderType="PX.Data.PXDatabaseMembershipProvider" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" applicationName="/" requiresUniqueEmail="false" passwordFormat="Clear" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="1" passwordAttemptWindow="10" passwordStrengthRegularExpression="" />
        </providers>
      </membership>
      <siteMap enabled="true" defaultProvider="PXWizardSiteMapProvider">
        <providers>
          <remove name="PXWikiProvider" />
          <remove name="PXWizardSiteMapProvider" />
          <remove name="PXDatabaseSiteMapProvider" />
          <remove name="MySqlSiteMapProvider" />
          <!-- that can be planted into machine.config by mysql .net connector installation -->
          <remove name="PXOuterSiteMapProvider" />
          <add name="PXDatabaseSiteMapProvider" type="PX.Data.PXDatabaseSiteMapProvider, PX.Data" securityTrimmingEnabled="true" table="PX.SM.SiteMap" />
          <add name="PXWizardSiteMapProvider" type="PX.Objects.WZ.PXWizardSiteMapProvider, PX.Objects" securityTrimmingEnabled="true" />
          <add name="PXWikiProvider" type="PX.Data.PXWikiProvider, PX.Data" securityTrimmingEnabled="true" />
        </providers>
      </siteMap>
    </system.web>
  </location>
</configuration>

so many details in app.config are needed because with shims you can't modify everything in your graph. 

Next goes code, that allows you to create any graph in case if your app.config is ready:

public class GraphCreator<Twhere T : PXGraphnew()
{
    public static T CreateInstance()
    {
        string userName = "admin";
        using (var pxLoginScope = new PXLoginScope(userName, new string[0]))
        {
            var membershipUser = (MembershipUsernull;
 
            membershipUser = Membership.GetUser(pxLoginScope.UserName);
            if (membershipUser != null)
            {
                pxLoginScope.UserName = membershipUser.UserName;
                userName = PXContext.PXIdentity.User.Identity.Name;
                if (pxLoginScope.CompanyName != null)
                {
                    string[] rolesForUser = System.Web.Security.Roles.GetRolesForUser(pxLoginScope.UserName);
                    if (rolesForUser == null || rolesForUser.Length == 0)
                        throw new PXException("You are not allowed to login to the company {0}."new object[1]
                        {
                            (object) pxLoginScope.CompanyName
                        });
                    else if (
                        !Enumerable.Contains<string>((IEnumerable<string>) PXDatabase.AvailableCompanies,
                            pxLoginScope.CompanyName))
                        throw new PXException("You are not allowed to login to the company {0}."new object[1]
                        {
                            (object) pxLoginScope.CompanyName
                        });
                }
                PXLogin.SetBranchID(pxLoginScope.UserName, pxLoginScope.CompanyName, pxLoginScope.Branch);
               
            }
        }
        var gr = PXGraph.CreateInstance<T>();
        return gr;
    }
}

With such preparation you can try to shim and unit test your code. While I should warn you it will not be an easy job.

2 Comments

  • Jonathan said

    Question Yuriy, are you able to integrate your Unit test into a CI/CD tool like TeamCity or something of that nature with this approach?

  • docotor said

    Hi Jonathan, no I didn't try to integrate it with CI/CD. But I suppose it can be great mental exercise. Also I think this is achievable task

Add a Comment
 

 

What Makes A Good Unit Test

 

Hello everybody,

today some notices of what is considered to be a good unit test.

1. Tests should be independent and isolated.  

For example if you have functions a, b, c tested, then sequence of test shouldn't affect the result. 

2. Each test should test single behaviour or logical staff. 

If to speak about phone example, calling and sending sms shouldn't be in one functoin

3. Clear purpose understood.

4. Don't test the compiler ( like writing/reading to db )

5. Reliable and repetable ( give the same result ).

6. Quality the same as other parts of solution.

7. Valuable for developers

No Comments

 

Add a Comment
 

 

What Are Asserts In Nunit

 

Hello everybody,

just short notice of NUnit function Assert.That

[Test]

public void CheckAddition()
{
  Assert.That(CalculatorClass.Minus (5, 2), Is.EqualTo(3));
}
public void CheckAddition()
{
   //old styel
Assert.AreEqual(3, CalculatorClass.Minus (5, 2));
}

No Comments

Add a Comment
 

 

Acumatica Unit Test

 

Hello readers of mine blog.

Today I want to share with everybody who wants to make unit test of Acumatica how I achieved it.

For unit testing I use NUnit. In order to start my work with Acumatica Unit testing I wrote the following class:

    [TestFixture]
    public class CATranEntryExtTest
    {
        [Test]
        public void TestAutonumber()
        {
            var gr = PXGraph.CreateInstance<CATranEntry>();
        }
    }

Now with help of Resharper let's start debugging:

And here we go, the first error message:

PX.Data.PXProviderException : Provider cannot be instantiated.

   at PX.Data.PXDatabase.get_Provider() in c:\Builders\4_10-2013_12_16-23_17_15-Full\Scripts\BuildTemp\NetTools\PX.Data\Database\Provider.cs: line 388

   at PX.Data.PXDatabase.GetSlot(String key, Type[] tables) in c:\Builders\4_10-2013_12_16-23_17_15-Full\Scripts\BuildTemp\NetTools\PX.Data\Database\Provider.cs: line 802

   at PX.Data.PXGraph.a(Type A_0) in c:\Builders\4_10-2013_12_16-23_17_15-Full\Scripts\BuildTemp\NetTools\PX.Data\Graph\Graph.cs: line 2684

   at PX.Data.PXGraph.CreateInstance(Type graphType) in c:\Builders\4_10-2013_12_16-23_17_15-Full\Scripts\BuildTemp\NetTools\PX.Data\Graph\Graph.cs: line 112

   at PX.Data.PXGraph.CreateInstance() in c:\Builders\4_10-2013_12_16-23_17_15-Full\Scripts\BuildTemp\NetTools\PX.Data\Graph\Graph.cs: line 57

So, it means we need to add some connection string to the site.  How to add it exactly? What proper way? I tried two next options:

1.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <remove name="ProjectX" />
    <add name="ProjectX" providerName="System.Data.SqlClient" connectionString="Data Source=DIO222\DIO195;Initial Catalog=Ac20140117;Integrated Security=False;User ID=sa;Password=1234" />
  </connectionStrings>
</configuration>



This way also:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionString value="Data Source=DIO222\DIO195;Initial Catalog=Ac20140117;Integrated Security=False;User ID=sa;Password=1234"></connectionString>
</configuration>

and if you suppose, that error message changed, you are wrong. It didn't go away.

Then I addressed support of Acumatica, and here is the answer:

 you just need this config. That is not necessary to create and use real database. You just need connectionString specified.

In case if with answer you know how to add connectionString to app.config for unit test and it will work, let me congratulate you. For me you are genious. Sadly to admit, I'm not. After a while, I decided to do crazy idea - copy/paste web.config into App.config. Imagine scale of my surprise when I noticed, that error went away!!!!!!

Now I got the next error message:

PX.Data.PXNotLoggedInException : You are not currently logged in.

Do you have any idea how to log in? 

In order to understand this idea lets go to Login.aspx.cs, maybe we can locate there something. For example method NormalLogin:

    private void NormalLogin(string[] companies)
    {
        if (companies != null && companies.Length == 1)
        {
            cmbCompany.Items.Clear();
            cmbCompany.Items.Add(companies[0]);
        }

        string loginText = txtUser.Text.Trim();
        string userName = PXDatabase.Companies.Length > 0 ? loginText + "@" +
            (cmbCompany.SelectedIndex != -1 ? cmbCompany.SelectedItem.Value : PXDatabase.Companies[0]) : loginText;

        if (!PXLogin.LoginUser(ref userName, txtPass.Text))
        {
            // we will change password during next round-trip
            PXContext.Session.SetString("ChangingPassword", txtPass.Text);

            DisablingUserPassword();
            EnablingChangingPassword();

            this.Master.Message = string.Empty;
        }
        else
        {
            PXLogin.InitUserEnvironment(userName, cmbLang.SelectedValue);
        }
    }

I'm considered about mehtod PXLogin.LoginUser. Lets use reflector in order to look there:

 

using (PXLoginScope pxLoginScope = new PXLoginScope(userName, new string[0]))using (PXLoginScope pxLoginScope = new PXLoginScope(userName, new string[0]))

I just think what can mean PXLoginScope. But maybe name is self-explanatory. Some scope where some login considered as working?

Lets write code similar to what reflector showed:

[Test]
public void TestAutonumber()
{
     using (var pxLoginScope = new PXLoginScope("someuserName", new string[0]))
     {
          var gr = PXGraph.CreateInstance<CATranEntry>();
     }
}

and now error message is 

PX.Data.PXUndefinedCompanyException : Unable determine proper company id for the request.

At least we can see, that error message changes during our thinking process :). 

All other conclusions were built on this code:

        MembershipUser membershipUser = (MembershipUser) null;
        try
        {
          if (Membership.ValidateUser(pxLoginScope.UserName, password))
          {
            membershipUser = Membership.GetUser(pxLoginScope.UserName);
            if (membershipUser != null)
            {
              pxLoginScope.UserName = membershipUser.UserName;
              userName = PXContext.PXIdentity.User.Identity.Name;
              if (pxLoginScope.CompanyName != null)
              {
                string[] rolesForUser = System.Web.Security.Roles.GetRolesForUser(pxLoginScope.UserName);
                if (rolesForUser == null || rolesForUser.Length == 0)
                  throw new PXException("You are not allowed to login to the company {0}.", new object[1]
                  {
                    (object) pxLoginScope.CompanyName
                  });
                else if (!Enumerable.Contains<string>((IEnumerable<string>) PXDatabase.AvailableCompanies, pxLoginScope.CompanyName))
                  throw new PXException("You are not allowed to login to the company {0}.", new object[1]
                  {
                    (object) pxLoginScope.CompanyName
                  });
              }
              if (PXLogin.c(pxLoginScope.UserName))
                return false;
              PXLogin.a(pxLoginScope.UserName, false);
              PXLogin.SetBranchID(pxLoginScope.UserName, pxLoginScope.CompanyName, pxLoginScope.Branch);
              PXSessionContextFactory.AuthenticateRequest();
              PXLicenseHelper.OnAuthenticate();
              return true;
            }

The function which has name a if to trust to reflector just updates some info in audit journal, and updates some info in db, which IMHO is not usable for unit test.

So, code, which I use now for initiating Unit test now looks like this:

    [TestFixture]
    public class CATranEntryExtTest
    {
        [Test]
        public void TestAutonumber()
        {
            string userName = "some user name in db";
            using (var pxLoginScope = new PXLoginScope(userName, new string[0]))
            {
                var membershipUser = (MembershipUser) null;
                // Membership.ValidateUser(pxLoginScope.UserName, "123");

                membershipUser = Membership.GetUser(pxLoginScope.UserName);
                if (membershipUser != null)
                {
                    pxLoginScope.UserName = membershipUser.UserName;
                    userName = PXContext.PXIdentity.User.Identity.Name;
                    if (pxLoginScope.CompanyName != null)
                    {
                        string[] rolesForUser = System.Web.Security.Roles.GetRolesForUser(pxLoginScope.UserName);
                        if (rolesForUser == null || rolesForUser.Length == 0)
                            throw new PXException("You are not allowed to login to the company {0}.", new object[1]
                                {
                                    (object) pxLoginScope.CompanyName
                                });
                        else if (
                            !Enumerable.Contains<string>((IEnumerable<string>) PXDatabase.AvailableCompanies,
                                                         pxLoginScope.CompanyName))
                            throw new PXException("You are not allowed to login to the company {0}.", new object[1]
                                {
                                    (object) pxLoginScope.CompanyName
                                });
                    }
                    PXLogin.SetBranchID(pxLoginScope.UserName, pxLoginScope.CompanyName, pxLoginScope.Branch);
                    var gr = PXGraph.CreateInstance<CATranEntry>();
                }
            }
          }
    }

At the end of execution gr looks like this:

Lets continue our way of unit testing for creating instance of our extension. This can be achieved via following way:

var accountsManager = gr.GetExtension<CATranEntryExt>();

Screenshot shows that we created instance of extension successfully:

Now I can test methods, which are inside of the extension CATranEntryExt.

I think it's enough for this article. If you have any comments, requests, wishes, critics, you are wormly welcome to express them.