[Question]: Best practice for Testing BC API's using the build process
Question
We're using more and more Unit Tests, but with regards to how it works with testing APIs (built into BC, either standard or our own custom APIs) in the AL-Go build process I'm a bit stuck. Given we need to authenticate against BC to access a page - how can we do that - is there something I am missing? Just looking for a bit guidance
Im currently on Vacation so forgive my briefness. But I think our current solution, is to use the hardcoded Username and Password that's used when a new container is created in the pipeline. The only drawback is that your local containers need the same credentials. That was for us the most practical way.
But the best way would probably be to create a new user before running the test. But you would also need to create the user in the database which is probably not easy from within the test.
Thanks, did think of that, but was wondering if I had missed something and maybe there was a more elegant solution 😀
My thoughts on this was to be able to check-in a postman or like collection to an AL-Go repo - and then we should run this collection against the BC container for testing. It is still in the backlog though.
But... - let me know a little about how you were thinking about running API tests - and also @jonaswre - how are you running API tests?
@freddydk we are running API test from within Test Codeunits that actually test e2e the API. A typical test would be generate some demo data json. Post it against an endpoint an check if that did create all the entites you expected. Doing this e2e gives as better confidence that things work as expected which is important since API pages are sometimes difficult to work with. (Order of events being triggered etc.)
Got it - so you are writing code to connect to "yourself" using httpClient ? Do you have a simple example of a test codeunit using this?
as you @freddydk mentioned postman: we kick'd off a small PoC a little while ago to make use of NewMan to process data from Postman. that worked kind of ok :) and allows you to bypass some postman API call limitations.
@KristofKlein I recently had this come up again. So I've created a little Codeunit to wrap some useful methods.
codeunit 99990 "BYD BDT Test API Helper"
{
var
LibraryRandom: Codeunit "Library - Random";
LibrarySetupStorage: Codeunit "Library - Setup Storage";
LibraryTestInitialize: Codeunit "Library - Test Initialize";
LibraryVariableStorage: Codeunit "Library - Variable Storage";
WebServiceKey: Text[50];
UserName: Text[50];
[Scope('OnPrem')]
procedure InitializeTest(TestCodeunitId: Integer)
var
User: Record User;
IdentityManagement: Codeunit "Identity Management";
begin
EnableHttpClientRequests();
LibraryTestInitialize.OnTestInitialize(TestCodeunitId);
ClearLastError();
LibraryVariableStorage.Clear();
LibrarySetupStorage.Restore();
LibraryTestInitialize.OnBeforeTestSuiteInitialize(TestCodeunitId);
LibraryRandom.Init();
// Set up web access key for first user
User.FindFirst();
WebServiceKey := IdentityManagement.CreateWebServicesKeyNoExpiry(User."User Security ID");
UserName := User."User Name";
LibraryTestInitialize.OnAfterTestSuiteInitialize(TestCodeunitId);
end;
procedure EnableHttpClientRequests()
var
NAVAppSetting: Record "NAV App Setting";
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
NAVAppSetting."App ID" := AppInfo.Id();
NAVAppSetting."Allow HttpClient Requests" := true;
if not NAVAppSetting.Insert() then
NAVAppSetting.Modify();
end;
procedure GetBasicAuthHeader(): Text
var
Convert: Codeunit "Base64 Convert";
Credentials: Text;
begin
Credentials := StrSubstNo('%1:%2', UserName, WebServiceKey);
exit(StrSubstNo('Basic %1', Convert.ToBase64(Credentials)));
end;
procedure GetUnboundActionUrl(ActionName: Text; Parameters: Dictionary of [Text, Text]): Text
var
CompanyInformation: Record "Company Information";
BaseUrl: Text;
Parameter: Text;
UrlBuilder: TextBuilder;
TenantId: Text;
begin
BaseUrl := GetUrl(ClientType::ODataV4);
CompanyInformation.Get();
// Extract tenant from base URL
TenantId := 'default'; // Default value
if BaseUrl.Contains('tenant=') then
TenantId := BaseUrl.Substring(BaseUrl.IndexOf('tenant=') + 7);
if TenantId.Contains('/') then
TenantId := TenantId.Substring(1, TenantId.IndexOf('/') - 1);
// Build URL with correct structure
UrlBuilder.Append(BaseUrl.Substring(1, BaseUrl.IndexOf('?') - 1)); // Base part without query
UrlBuilder.Append('/');
UrlBuilder.Append(ActionName);
UrlBuilder.Append('?tenant=');
UrlBuilder.Append(TenantId);
UrlBuilder.Append('&company=');
UrlBuilder.Append(GetUrlEncodedCompanyName());
// Add additional parameters
foreach Parameter in Parameters.Keys() do begin
UrlBuilder.Append('&');
UrlBuilder.Append(Parameter);
UrlBuilder.Append('=');
UrlBuilder.Append(Parameters.Get(Parameter));
end;
exit(UrlBuilder.ToText());
end;
procedure GetUrlEncodedCompanyName(): Text
var
TypeHelper: Codeunit "Type Helper";
CompanyN: Text;
begin
CompanyN := CompanyName();
TypeHelper.UrlEncode(CompanyN);
exit(CompanyN);
end;
procedure GetResponseContent(var ResponseMessage: HttpResponseMessage) Content: Text
var
InStr: InStream;
begin
ResponseMessage.Content.ReadAs(InStr);
InStr.ReadText(Content);
end;
}
There is some stuff that you might not need but this might point you into the right direction.