My TDD Technique for Backend API development with Xunit (Custom Attribute, WebClient GET/POST, JWT auth, Fact, Theory, InlineData, ClassData iterator function, Inject Log, Txt parsers for console output)
I have a couple articles about TDD, for example
- (2022)Asynchronous MultiThreaded SSH engine for Web (Net Core 6, Linux) - Part 9,10 (BackendAPI for NotificationController and my technique to testing this engine with xUnit).
- (2020)TDD in Core Linux (Middleware, IdentityServer, SignalR, Quartz, RabbitMQ, Redis and so on)
- (2013) TDD - Test Driven Development
- (2016) Unit-тести для ASP.NET MVC
this is continue about my way to development huge project step-by-step.
- 1. Project State.
- 2. Authentication JWT token.
- 3. Inject Log.
- 4. My technique to testing GET/POST API.
- 5. Repeat test for various data - manually and InlineData with iterator function.
- 6. Txt parsers debug.
- 7. Debug test.
- 8. Update testing GET/POST API with Generic function.
- 9. Update ProjectState. Arrange API to security layers.
1. Project State.
This is my technology for support development process. Firstly, I have created this attribute
1: <AttributeUsage(AttributeTargets.Method)>
2: Public Class StateAttribute
3: Inherits Attribute
4:
5: Public ReadOnly Value
6: Public Sub New(value As String)
7: Me.Value = value
8: End Sub
9:
10: End Class
And than I have mark each API one-by-one on the way of development process.
Than I have created this simplest test.
1: Imports System.Reflection
2: Imports Xunit
3: Imports Xunit.Abstractions
4:
5: Public Class ProjectState
6: Private ReadOnly Log As ITestOutputHelper
7:
8: Public Sub New(_Log As ITestOutputHelper)
9: Log = _Log
10: End Sub
11:
12: <Fact>
13: Sub GetApiState()
14: Dim AllApi As Integer, OkApi As Integer, NfApi As Integer
15: Dim API As Assembly = Assembly.LoadFile("G:\Projects\CryptoChestMax\BackendAPI\BackendAPI\bin\Debug\net6.0\BackendAPI.dll")
16: Dim Types = API.GetTypes()
17: Dim Controllers As List(Of Type) = Types.Where(Function(X) X.Name.Contains("Controller")).ToList
18: For i As Integer = 0 To Controllers.Count - 1
19: Log.WriteLine($"{i + 1}. {Controllers(i).Name.Replace("Controller", "")}")
20: Dim Methods As List(Of MethodInfo) = DirectCast(Controllers(i), System.Reflection.TypeInfo).DeclaredMethods.ToList
21: For j As Integer = 0 To Methods.Count - 1
22: Dim Attributes As List(Of CustomAttributeData) = Methods(j).CustomAttributes.ToList
23: For k = 0 To Attributes.Count - 1
24: If Attributes(k).AttributeType.Name = "StateAttribute" Then
25: Dim Val As String = Attributes(k).ToString.Replace("[BackendAPI.StateAttribute(", "").Replace(")]", "")
26: Log.WriteLine($" {j + 1}. {Methods(j).Name} : {Val}")
27: AllApi += 1
28: If Val.ToLower.Contains("ok.") Then
29: OkApi += 1
30: ElseIf Val.ToLower.Contains("api not found") Then
31: NfApi += 1
32: End If
33: End If
34: Next
35: Next
36: Next
37: Log.WriteLine($"Summary: Total {AllApi}, Finished {OkApi}, NotFound {NfApi}.")
38: End Sub
39: End Class
And receive this result.
2. Authentication JWT token.
Usually Backend API need authorization, therefore first point is receive correct JWT token to perform API. This is my way to make this task .
1: Imports System.Net
2:
3: Public Class MyWebClient
4: Inherits WebClient
5: Protected Overloads Function GetWebRequest(URL As Uri) As WebRequest
6: Dim WebRequest = MyBase.GetWebRequest(URL)
7: WebRequest.ContentType = "application/json"
8: WebRequest.Timeout = Integer.MaxValue
9: Return WebRequest
10: End Function
11: End Class
1: Imports System.Net
2: Imports System.Net.Http
3: Imports System.Text
4: Imports BackendAPI
5: Imports Newtonsoft.Json
6:
7: Module Common
8: Public Function GetAdminToken(Request As MyWebClient) As String
9: Dim PostPrm = New BackendAPI.Model.AuthenticateRequest With {
10: .Username = "XXXXX",
11: .Password = "YYYYYYYYYYYYYYYYY"
12: }
13: Dim PostData = JsonConvert.SerializeObject(PostPrm)
14: Dim Response = Encoding.UTF8.GetString(Request.UploadData("/Users/Authenticate", Encoding.UTF8.GetBytes(PostData)))
15: Dim Ret1 = JsonConvert.DeserializeObject(Response)
16: Return Ret1("token").ToString
17: End Function
18:
19: Public Function GetUserToken(Request As MyWebClient, Username As String, Password As String) As String
20: Dim PostPrm = New BackendAPI.Model.AuthenticateRequest With {
21: .Username = Username,
22: .Password = Password
23: }
24: Dim PostData = JsonConvert.SerializeObject(PostPrm)
25: Dim Response = Encoding.UTF8.GetString(Request.UploadData("/Users/Authenticate", Encoding.UTF8.GetBytes(PostData)))
26: Dim Ret1 = JsonConvert.DeserializeObject(Response)
27: Return Ret1("token").ToString
28: End Function
29:
30: End Module
3. Inject Log.
Log injected to Xunit as any other NET CORE Module.
1: Public Class Container
2: Friend ReadOnly BaseUrl As String = "http://localhost:4000/"
3: Friend ReadOnly Request As MyWebClient
4: Private ReadOnly Log As ITestOutputHelper
5:
6: Public Sub New(_Log As ITestOutputHelper)
7: Request = New MyWebClient
8: Request.BaseAddress = BaseUrl
9: Request.Headers.Add("Content-Type", "application/json")
10: Log = _Log
11: End Sub
4. My technique to testing GET/POST API.
Standard testing technique mean usind WebApplicaionFactory.
Its possible, but not best way, this is not full complex technique to examine application, therefore I use little bit another way. This is my Technique to testing API.
1: <Fact>
2: Sub GetMasternodeInfo()
3: Dim Token = GetAdminToken(Request)
4: Request.Headers.Add("Content-Type", "application/json")
5: Request.Headers.Add("Authorization", "Bearer: " & Token)
6: Try
7: Dim Response = Request.DownloadString("/DbMasternode/GetMasternodeInfo")
8: Log.WriteLine(Response)
9: Catch ex As WebException
10: Dim Resp As String = ""
11: Dim Stream = ex.Response?.GetResponseStream()
12: If Stream IsNot Nothing Then
13: Dim Sr = New IO.StreamReader(Stream)
14: Resp = Sr.ReadToEnd
15: End If
16: Log.WriteLine(Resp & vbCrLf & ex.Message)
17: End Try
18: End Sub
You can also skip some test.
And this is template to perform POST.
1: <Fact>
2: <InlineData(1, 1, 1, "/root/.xsncore/xsn.tst", "/root/.xsncore/xsn-cli getstakingstatus", "tst", "............")>
3: Sub AddMasternodeInfo(toVm As Integer, toUser As Integer, toCoin As Integer, ConfigPath As String, StatusCommand As String, Comment As String, Config As String)
4: Dim Token = GetAdminToken(Request)
5: Request.Headers.Add("Content-Type", "application/json")
6: Request.Headers.Add("Authorization", "Bearer: " & Token)
7: Dim PostPrm = New SetUpMasterNodeRequest With {
8: .toVm = toVm,
9: .toUser = toUser,
10: .toCoin = toCoin,
11: .ConfigPath = ConfigPath,
12: .StatusCommand = StatusCommand,
13: .Comment = Comment
14: }
15: Dim JsonSerializer = New JsonSerializer()
16: Dim PostData = JsonConvert.SerializeObject(PostPrm)
17: Try
18: Dim Response = Encoding.UTF8.GetString(Request.UploadData("/DbMasternode/AddMasternodeInfo", Encoding.UTF8.GetBytes(PostData)))
19: Log.WriteLine(Response)
20: Catch ex As WebException
21: Dim Resp As String = ""
22: Dim Stream = ex.Response?.GetResponseStream()
23: If Stream IsNot Nothing Then
24: Dim Sr = New IO.StreamReader(Stream)
25: Resp = Sr.ReadToEnd
26: End If
27: Log.WriteLine(Resp & vbCrLf & ex.Message)
28: End Try
29: End Sub
On the test above you can see how to pass test parameters with Fact and InlineData.
5. Repeat test for various data - manually and InlineData with iterator function.
You can repeat test with various data manually
And generate serial parameters with ClassData Iterator function.
1: Public Class DeleteVolumeParm
2: Implements IEnumerable(Of Object())
3:
4: Public Iterator Function GetEnumerator() As IEnumerator(Of Object()) Implements IEnumerable(Of Object()).GetEnumerator
5: For I As Integer = 1 To 11
6: Yield New Object() {"/dev/sdb" & I, True}
7: Next
8: For I As Integer = 1 To 6
9: Yield New Object() {"/dev/sdc" & I, True}
10: Next
11: For I As Integer = 8 To 11
12: Yield New Object() {"/dev/sdc" & I, True}
13: Next
14: For I As Integer = 1 To 6
15: Yield New Object() {"/dev/sdd" & I, True}
16: Next
17: Yield New Object() {"/dev/sdd9", True}
18: End Function
19:
20: Private Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
21: Return GetEnumerator()
22: End Function
23: End Class
24:
25: <Theory>
26: <ClassData(GetType(DeleteVolumeParm))>
27: Sub DeleteVolumeFromDB(Path As String, WithKvmDevicePartition As Boolean)
28: ....
6. Txt parsers debug.
Also I use test project to test various text parser and than insert to main project only correct program code. This is example.
7. Debug test.
If you want force start debugger (even test working in run mode) you can add this line.
System.Diagnostics.Debugger.Launch()
8. Update testing GET/POST API with Generic function.
This updating allow me extremely simplify testing code. And my testing code looks as:
So, this is my testing function:
1: Imports BackendAPI
2: Imports Newtonsoft.Json
3: Imports System.Net
4: Imports System.Runtime.CompilerServices
5: Imports System.Text
6: Imports Xunit.Abstractions
7:
8: Module RequestExtension
9: <Extension>
10: Public Sub PostRequest(Of T)(Request As MyWebClient, Log As ITestOutputHelper, Token As String, URL As String, PostPrm As T)
11: Request.Headers.Add("Content-Type", "application/json")
12: Request.Headers.Add("Authorization", "Bearer: " & Token)
13: Dim JsonSerializer = New JsonSerializer()
14: Dim PostData = JsonConvert.SerializeObject(PostPrm)
15: Try
16: Dim Response = Encoding.UTF8.GetString(Request.UploadData(URL, Encoding.UTF8.GetBytes(PostData)))
17: Log.WriteLine(Response)
18: Dim TmpDir As String = System.IO.Path.GetTempPath()
19: Dim TmpFile As String = IO.Path.Combine(TmpDir, Replace(Replace(Replace(URL, "/", "-"), "&", "-"), "?", "-"))
20: IO.File.WriteAllText(TmpFile & ".json", Response)
21: Dim Ret1 = Process.Start("C:\Program Files\Mozilla Firefox\firefox.exe", TmpFile & ".json")
22: Catch ex As WebException
23: Dim Resp As String = ""
24: Dim Stream = ex.Response?.GetResponseStream()
25: If Stream IsNot Nothing Then
26: Dim Sr = New IO.StreamReader(Stream)
27: Resp = Sr.ReadToEnd
28: End If
29: Log.WriteLine(Resp & vbCrLf & ex.Message)
30: End Try
31: End Sub
32:
33: <Extension>
34: Public Sub GetRequest(Request As MyWebClient, Log As ITestOutputHelper, Token As String, URL As String)
35: Request.Headers.Add("Content-Type", "application/json")
36: Request.Headers.Add("Authorization", "Bearer: " & Token)
37: Try
38: Dim Response = Request.DownloadString(URL)
39: Log.WriteLine(Response)
40: Dim TmpDir As String = System.IO.Path.GetTempPath()
41: Dim TmpFile As String = IO.Path.Combine(TmpDir, Replace(Replace(Replace(URL, "/", "-"), "&", "-"), "?", "-"))
42: IO.File.WriteAllText(TmpFile & ".json", Response)
43: Dim Ret1 = Process.Start("C:\Program Files\Mozilla Firefox\firefox.exe", TmpFile & ".json")
44: Catch ex As WebException
45: Dim Resp As String = ""
46: Dim Stream = ex.Response?.GetResponseStream()
47: If Stream IsNot Nothing Then
48: Dim Sr = New IO.StreamReader(Stream)
49: Resp = Sr.ReadToEnd
50: End If
51: Log.WriteLine(Resp & vbCrLf & ex.Message)
52: End Try
53: End Sub
54:
55: <Extension>
56: Public Sub GetAnonRequest(Request As MyWebClient, Log As ITestOutputHelper, URL As String)
57: Try
58: Dim Response = Request.DownloadString(URL)
59: Log.WriteLine(Response)
60: Dim TmpDir As String = System.IO.Path.GetTempPath()
61: Dim TmpFile As String = IO.Path.Combine(TmpDir, Replace(Replace(Replace(URL, "/", "-"), "&", "-"), "?", "-"))
62: IO.File.WriteAllText(TmpFile & ".json", Response)
63: Dim Ret1 = Process.Start("C:\Program Files\Mozilla Firefox\firefox.exe", TmpFile & ".json")
64: Catch ex As WebException
65: Dim Resp As String = ""
66: Dim Stream = ex.Response?.GetResponseStream()
67: If Stream IsNot Nothing Then
68: Dim Sr = New IO.StreamReader(Stream)
69: Resp = Sr.ReadToEnd
70: End If
71: Log.WriteLine(Resp & vbCrLf & ex.Message)
72: End Try
73: End Sub
74: End Module
Additional advance that I can receive and analyze JSON result of each test function directly ion browser.
9. Update ProjectState. Arrange API to security layers.
Look to this page please Simplest way to create tracing and authorization layers in Backend API. Special attribute to arrange API for authorization layers..
<SITEMAP> <MVC> <ASP> <NET> <DATA> <KIOSK> <FLEX> <SQL> <NOTES> <LINUX> <MONO> <FREEWARE> <DOCS> <ENG> <CHAT ME> <ABOUT ME> < THANKS ME> |