HERE

Build a Service Adapter with qTrace Integration API 2.0

Last week in the series about qTrace Integration API, we examined the Publisher API, now it’s time to look at the high level Service Adapter API.

First, let’s answer a question: why do we need this API? Isn’t it the case that every integration needed between qTrace and any other system can be built with the Publisher API already? That’s entirely true. In fact, if your integration is pretty unique and you need much flexibility, then you should stick to the Publisher API. However, if you want to build a publisher that is pretty similar to built-in publishers of qTrace, it will take a lot of effort using that API.

The reason is because qTrace’s built-in publishers come with far more features than just the ability to submit a defect. For example, built-in publishers support field dependencies, allow users to change field value, modify default values, show/hide fields, integrate with Dropbox, add more attachments etc. Replicating all these things while maintaining a consistent user experience is no trivial task. Therefore, we built the Service Adapter API so that you can easily build a custom publisher on top of the API while reusing most of the functionality you see built-in publishers have.

The basic idea behind this API is that you, as the integrator, should only have to worry about consuming the services exposed by the backend system while leaving qTrace to take care of all UI-related, thread synchronization, Dropbox integration and so on. So when you write code against this API, your code mostly deals with network-related and business logic and rarely, if at all, has to deal with UI logic.

In this post, I’m going to show you how to use the Service Adapter API to build a custom publisher. To keep things simple, we’re not going to integrate qTrace with any actual backend system. Instead, our custom publisher will use fake data, as though they come from a web service of a backend system. Despites the source of data, we’ll see our publisher work seamlessly with all amazing features the Service Adapter API supports. Let’s get started.

Setup

Start Visual Studio .NET and create a new library project, name it SampleAdapter.

Add references to the following assemblies (the first is shipped with qTrace, the second is from .NET framework)

  • qTrace.Publishing.Contracts
  • System.ComponentModel.Composition

Add the following post-build event to the project.

Create a service adapter

Create a class named ServiceAdapter and implements the IServiceAdapter interface. Make sure you include the ExportAttribute for the class.

namespace SampleAdapter
{
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using qTrace.Publishing.Contracts;

    [Export(typeof(IServiceAdapter))]
    public class ServiceAdapter : IServiceAdapter
    {
        public PublisherInfo GetMetadata()
        {
            throw new System.NotImplementedException();
        }

        public List GetIndependentFields(PublisherSettings settings)
        {
            throw new System.NotImplementedException();
        }

        public List GetDependentFields(PublisherSettings settings, string parentFieldName, string parentFieldValue)
        {
            throw new System.NotImplementedException();
        }

        public VerificationResult Verify(PublisherSettings settings)
        {
            throw new System.NotImplementedException();
        }

        public PublishingResult Publish(PublisherSettings settings, PublishingRecord record, IDictionary fieldsValues, List attachments)
        {
            throw new System.NotImplementedException();
        }
    }
}

GetMetadata()

Similar to IPublisher, this method is used by a service adapter to give qTrace information about itself. You can learn more about this method in the previous blog post. For now, we simply specify the name and 2 icons. Note that we use 2 icons here, the small one is displayed in Settings screen and Submit button while the large one is used in the Submission screen’s banner area. If the large icon isn’t specified, the small icon is used. Make sure the icons’ Built Action is set to Resource.

public PublisherInfo GetMetadata()
{
    return new PublisherInfo {
        SmallIconUri = "/SampleAdapter;component/small.png",
        LargeIconUri = "/SampleAdapter;component/large.png",
        DisplayName = "Sample Adapter"
    };
}

Verify()

qTrace invokes this method to verify the information user provides in the Settings screen. Unlike when implementing IPublisher, you don’t have to care about showing/hiding progress indicator (you can’t do that anyway because you don’t have access to an IPublishingContext like IPublisher does). In an actual adapter, you will attempt to connect to the backend system with the supplied credentials. For our purpose, we will simulate this authentication progress.

public VerificationResult Verify(PublisherSettings settings)
{
    // Simulate connecting...
    Thread.Sleep(2000);

    if (settings.UserName == "qtrace" && settings.Password == "qtrace")
        return new VerificationResult();

    throw new Exception("Not authenticated!");
}

Go building your adapter. If the Post-Build Event is set correctly, the adapter assembly will be copied to your qTrace’s installation folder and thus ready to go. Launch qTrace and go to Settings screen. Create a new connection for our adapter. Try input both valid (i.e qtrace/qtrace) and invalid credentials, you should see qTrace provides the correct status to users. In addition, progress indicator is automatically shown and hidden by qTrace.

GetIndependentFields() and GetDependentFields()

This pair of methods is really key for a service adapter. qTrace uses these methods to know what fields the service adapter support, the dependencies among them, their type and values and so on. With that knowledge, qTrace will then render appropriate screens and controls as well as allow users to customize settings such as default value, show/hide status etc.

Normally, your service adapter will go out talking to some backend service in order to populate the returned data expected by qTrace. For our sample adapter, we will simulate this communication and return some fake data instead.

First, what should GetIndependentFields() return? It returns the metadata for those fields whose values don’t depend on the value of any other field. Let’s consider a typical defect tracker. Fields like Project, Description, Due Date don’t depend on any other field. GetIndependentFields() should therefore return the metadata for these fields. On the other hand, fields like Priority, Component, Version etc. are depedent on the selected Project. The metadata of these fields are to be returned by GetDependentFields(). Besides, among the independent fields, some of them drive the values of dependent fields and some don’t. You can specify that in the returned metadata so that whenever the value of such fields is changed, qTrace will attempt to invoke GetDependentFields() to reload the values of dependent fields.

Specifying field dependency is among the things that can be controlled with metadata returned by these two methods. You can also specify the data type of the field (so that qTrace can render it properly), change its display order, force a field to always appear in Submission screen or specify whether a field can have its default value changed.

Below is key types and properties of this field metadata model.

With that knowledge in mind, let’s implement the methods based on the following field description (note that some configurations are left in for demonstration purpose instead of being under practical demand):

  • Project: independent, required and must always be shown in Submission screen
  • Due Date: indepedent, shown in Submission screen by default
  • Priority: dependent on Project, shown in Submission screen by default

The implementation for this model is as follows.

public List GetIndependentFields(PublisherSettings settings)
{
    // Simulate connecting...
    Thread.Sleep(1000);

    return new List {
        new Field {
            Name = "project",
            DisplayName = "Project",
            DisplayOrder = 1,
            IsFoundationField = true,
            HasDependentFields = true,
            FieldType = FieldType.SingleChoice,
            AcceptedValues = new List {
                new AcceptedValue {
                    Id = "1",
                    Value = "qTrace"
                },
                new AcceptedValue {
                    Id = "2",
                    Value = "qTest"
                },
            }
        },
        new Field {
            Name = "date",
            DisplayName = "Due Date",
            DisplayOrder = 2,
            ShowInSubmissionScreen = true,
            FieldType = FieldType.Date,
            IsDefaultValueEditable = false
        }
    };
}

public List GetDependentFields(PublisherSettings settings, string parentFieldName, string parentFieldValue)
{
    // Simulate connecting...
    Thread.Sleep(1000);

    if (parentFieldName != "project") return new List();

    return new List {
        new Field {
            Name = "priority",
            DisplayName = "Priority",
            DisplayOrder = 3,
            FieldType = FieldType.SingleChoice,
            ShowInSubmissionScreen = true,
            AcceptedValues = new List {
                new AcceptedValue {
                    Id = "1",
                    Value = parentFieldValue == "1" ? "Major" : "High"
                },
                new AcceptedValue {
                    Id = "2",
                    Value = "Medium"
                },
                new AcceptedValue {
                    Id = "3",
                    Value = parentFieldValue == "1" ? "Minor" : "Low"
                },
            }
        }
    };
}

Note the followings:

  • Project field: IsFoundationField is true, meaning this field is always shown and won’t appear in the Fields Settings screen (can’t modify its default value, can’t hide it); HasDependentFields is true, meaning whenever the value of this field is changed by users, qTrace will invoke GetIndependentFields() with the name and selected value of it.
  • Priority field: the AcceptedValues varies depending on the currently selected project.
  • DisplayOrder of fields should be specified to explicity control their relative orders; not specifying DisplayOrder will have unspecified behavior due to the complicated interweaving logic among field dependencies, show/hide status, explicit order etc.
  • ShowInSubmissionScreen is true for Due Date and Priority, meaning they will appear in the Submission screen by default (although users can hide them in the Fields Settings screen).
  • IsDefaultValueEditable is true for Due Date, meaning users can’t change the default value for this field. Instead, qTrace will persist the last selected value and use it in subsequent submissions.

Build the adapter and launch qTrace. Capture a defect and click Submit. qTrace will render the adapter based on the returned field metadata. Select another project and you’ll see qTrace reload and update the priority list. Best of all, we have access to many goodies like Dropbox integration, additional file attachment, fields settings etc.

Submit()

Now that we have allowed users to manage fields, attachments and select values for submission, we’re ready to implement the Submit() method. This method receives selected field values, qTrace attachments and attachment options and submit everything to a backend system. Normally, this means posting to a REST endpoint or invoking some XML web service. Below is a brief description of the parameters for this method:

  • settings: information provided by users in the Settings screen (or any additional info stored by any method of this service adapter)
  • record: include title, description, trace steps and environment information.
  • fieldsValues: the map of field name and their value(s). Note that value is an array, values of single-valued fields are stored in the first position.
  • attachments: list of qTrace attachments. Note that as users might have selected to embed only link or Dropbox link of some attachments (as opposed to attach their entire files), your code must take this into account when submitting the defect.

For our purpose, we simply return the success status with a generated defect ID which is shown to users.

public PublishingResult Publish(
    PublisherSettings settings,
    PublishingRecord record,
    IDictionary fieldsValues,
    List attachments)
{
    // Simulate connecting...
    Thread.Sleep(1000);

    return new PublishingResult {
        RecordId = new Random().Next(1, 100).ToString(),
        Message = "Published successfully!"
    };
}

Build the library and launch qTrace. Capture and submit a defect, you should see the following screen.

That’s everything. Below is the full code example for our sample service adapter.

namespace SampleAdapter
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Threading;
    using qTrace.Publishing.Contracts;

    [Export(typeof(IServiceAdapter))]
    public class ServiceAdapter : IServiceAdapter
    {
        public PublisherInfo GetMetadata()
        {
            return new PublisherInfo {
                SmallIconUri = "/SampleAdapter;component/small.png",
                LargeIconUri = "/SampleAdapter;component/large.png",
                DisplayName = "Sample Adapter"
            };
        }

        public VerificationResult Verify(PublisherSettings settings)
        {
            // Simulate connecting...
            Thread.Sleep(1000);

            if (settings.UserName == "qtrace" && settings.Password == "qtrace")
                return new VerificationResult();

            throw new Exception("Not authenticated!");
        }

        public List GetIndependentFields(PublisherSettings settings)
        {
            // Simulate connecting...
            Thread.Sleep(1000);

            return new List {
                new Field {
                    Name = "project",
                    DisplayName = "Project",
                    DisplayOrder = 1,
                    IsFoundationField = true,
                    HasDependentFields = true,
                    FieldType = FieldType.SingleChoice,
                    AcceptedValues = new List {
                        new AcceptedValue { Id = "1", Value = "qTrace" },
                        new AcceptedValue { Id = "2", Value = "qTest" }
                    }
                },
                new Field {
                    Name = "date",
                    DisplayName = "Due Date",
                    DisplayOrder = 2,
                    ShowInSubmissionScreen = true,
                    FieldType = FieldType.Date,
                    IsDefaultValueEditable = false
                }
            };
        }

        public List GetDependentFields(PublisherSettings settings, string parentFieldName, string parentFieldValue)
        {
            // Simulate connecting...
            Thread.Sleep(1000);

            if (parentFieldName != "project") return new List();

            return new List {
                new Field {
                    Name = "priority",
                    DisplayName = "Priority",
                    DisplayOrder = 3,
                    FieldType = FieldType.SingleChoice,
                    ShowInSubmissionScreen = true,
                    AcceptedValues = new List {
                        new AcceptedValue {
                            Id = "1",
                            Value = parentFieldValue == "1" ? "Major" : "High"
                        },
                        new AcceptedValue {
                            Id = "2",
                            Value = "Medium"
                        },
                        new AcceptedValue {
                            Id = "3",
                            Value = parentFieldValue == "1" ? "Minor" : "Low"
                        },
                    }
                }
            };
        }

        public PublishingResult Publish(
            PublisherSettings settings,
            PublishingRecord record,
            IDictionary fieldsValues,
            List attachments)
        {
            // Simulate connecting...
            Thread.Sleep(1000);

            return new PublishingResult {
                RecordId = new Random().Next(1, 100).ToString(),
                Message = "Published successfully!"
            };
        }
    }
}

Conclusion

This marks the end of the series about qTrace Integration API 2.0 Preview. We have looked at how this API evolves significantly from version 1.0 as well as examined in details how to build custom qTrace publishers on top of the 2 sub APIs.

While the Publisher API provides the backbone framework for publishers of any level of sophistication to be built, the Service Adapter API provides a generic framework with all standard qTrace services provided out of the box. When developing a custom publisher for qTrace, I recommend you start with the Service Adapter API and only consider the Publisher API should the former doesn’t do what you want. But note, as we improve qTrace and build more integration, the ServiceAdapter API will evolve to be more powerful and at the same time more tolerated of specific workflows and rules.

I hope you have enjoyed the series. You can access to the full VS.NET project from GitHub. Note that this is a preview release of the API, by the time qTrace 2.6 is released, it might have had some changes. Please leave a comment or email info@qasymphony.com if you have any question/suggestion.

Leave a Reply

Your email address will not be published. Required fields are marked *

More Great Content

Get Started with QASymphony