TDD - Test Driven Development
There are many alternative technology of create high quality code Program Theory, but there are a couple of different approach to debugging and testing software.
- Main method forever is interactive debugging.
- Alternative method is create a lot of log with tracing calling sequence and important data (usually at least need trace input and output parameters). Tracing log method usually I apply for payment gateway development, because remote debugging gateway is difficult and raise a timeout.
- Third method is using parametrization to perform fake stub for testing purpose to the code to be testing.
- And fourth main method of testing and debugging is TDD (Test Driven Development). This method usually based to redefinition some part of code for testing purpose by OOP opportunities. See practical use of this method in my pages TDD in Core Linux (Middleware, IdentityServer, SignalR, Quartz, RabbitMQ, Redis and so on), Unit-тести для ASP.NET MVC.
I do not use in project below a separate testing project, because this project is interactive console application. It is possible to emulate console in test project, but this is a time and efforts. More simple way is use test in the same project with replace various part of code by stub. This way is not accessible alway, for example for continuous deployment special test project is mandatory. But for one developer separate test project is not really a good choice.
Testing drive technology I will show in my program to transformation Russian language site to MultiLanguages site. Real site has thousand pages and a lot. really a lot text literals.
I decide manually create list of file for analyzing, because I want to mark processed files and manually input list is better for TDD. Also I manually created a needed resource in App_GlobalResources (I create empty resource file and copy this file with the needed name to folder App_GlobalResources) - because before I start write this program I pass some files manually. Only after I have understanding my wrong manually way, I decide to continue this task with this program and farther I decide create this article as example of TDD)
Therefore manually this job is impossible. Another mandatory future of this program is translating cache. Because Google and MS Translater distort text, finally I decide change automatically translate to manually edited. Also I decide cache of translated and edited litherals.
Result of my program is changing VB source code and add resource to RESX- file.
So, I hope goal of this program is aware for reader, and I move to the literally TDD. What is means? It means special method to writing program when each function can be tested separately, with full isolation from other part of code. But firstly,please look to my code. It has two classes.
First class is TextService, firstly I add to thos class automatically google translator, and then redefine it to manual translator. And second method is transform Englist text to resource name.
1: Imports System.Net.Http
2: Imports System.Collections
3: Imports System.Web.Script.Serialization
4: Imports System.Text.RegularExpressions
5: Imports System.Windows.Forms
6:
7: Public Class FakeTextService
8: Inherits TextService
9:
10: Public Overloads Function GoogleTranslateText(ByVal input As String) As String
11: Clipboard.SetText(Mid(input, 2, input.Length - 2))
12: Console.BackgroundColor = ConsoleColor.Yellow
13: Console.Write("EN>")
14: Console.BackgroundColor = ConsoleColor.Black
15: Return Console.ReadLine.Replace(vbCrLf, "")
16: End Function
17:
18: End Class
19:
20:
21: Public Class TextService
22:
23: Public Function TranslatedCacheIsExist(OneRusLiteral As String, ByRef GoogleTranslatedPhrase As String, ByRef ResourseName As String) As Boolean
24: Dim CacheFiles() As String = IO.File.ReadAllLines("E:\Projects\Arenda_5\MyArenda\TextToResourse\TextToResourse\Cache.txt")
25: For I As Integer = 0 To CacheFiles.Count - 1 Step 3
26: If OneRusLiteral = CacheFiles(I) Then
27: Console.Write(" (cache) " & CacheFiles(I + 1))
28: GoogleTranslatedPhrase = CacheFiles(I + 1)
29: ResourseName = CacheFiles(I + 2)
30: Return True
31: End If
32: Next
33: Return False
34: End Function
35:
36: Public Sub TranslatedCacheAdd(OneRusLiteral As String, TranslatedPhrase As String, ResourseName As String)
37: Dim CacheFiles() As String = IO.File.ReadAllLines("E:\Projects\Arenda_5\MyArenda\TextToResourse\TextToResourse\Cache.txt")
38: Array.Resize(CacheFiles, CacheFiles.Length + 3)
39: CacheFiles(CacheFiles.Length - 3) = OneRusLiteral.Replace(vbCrLf, "")
40: CacheFiles(CacheFiles.Length - 2) = TranslatedPhrase.Replace(vbCrLf, "")
41: CacheFiles(CacheFiles.Length - 1) = ResourseName
42: IO.File.WriteAllLines("E:\Projects\Arenda_5\MyArenda\TextToResourse\TextToResourse\Cache.txt", CacheFiles)
43: End Sub
44:
45: Public Function PhraseToName(Phrase As String) As String
46: Dim MultiWordLiteral As String = ""
47: Dim Reg2 As New Regex("( [a-z])")
48: Dim MultiWord As MatchCollection = Reg2.Matches(Phrase)
49: If MultiWord.Count > 0 Then
50: MultiWordLiteral = Reg2.Replace(Phrase, Function(X) X.Value.ToString.Trim.ToUpper)
51: Else
52: MultiWordLiteral = Phrase
53: End If
54: Dim Reg3 As New Regex("([A-Z]|[a-z])*")
55: Dim Words As MatchCollection = Reg3.Matches(MultiWordLiteral)
56: Dim OnlyLetters As String = ""
57: For Each OneWord As Match In Words
58: OnlyLetters &= OneWord.Value
59: Next
60: Return OnlyLetters
61: End Function
62:
63: 'Translate.googleapis.com site use is very limited. It only allows about 100 requests per one hour
64: Public Function GoogleTranslateText(ByVal input As String) As String
65: ' Set the language from/to in the url (or pass it into this function)
66: Dim url As String = String.Format("https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}", "ru", "en", Uri.EscapeUriString(input))
67: Dim httpClient As HttpClient = New HttpClient()
68: Dim result As String
69: Try
70: result = httpClient.GetStringAsync(url).Result
71: Catch ex As System.AggregateException
72: Console.WriteLine(ex.InnerException)
73: Return ""
74: End Try
75: If result = "" Then
76: Threading.Thread.Sleep(500)
77: Return ""
78: End If
79: ' Get all json data
80: Dim jsonData = New JavaScriptSerializer().Deserialize(Of List(Of Object))(result)
81: ' Extract just the first array element (This is the only data we are interested in)
82: Dim translationItems = jsonData(0)
83: 'Translation Data
84: Dim translation As String = ""
85: 'Loop through the collection extracting the translated objects
86: For Each item As Object In translationItems
87: 'Convert the item array to IEnumerable
88: Dim translationLineObject As IEnumerable = TryCast(item, IEnumerable)
89: 'Convert the IEnumerable translationLineObject to a IEnumerator
90: Dim translationLineString As IEnumerator = translationLineObject.GetEnumerator()
91: 'Get first object in IEnumerator
92: translationLineString.MoveNext()
93: 'Save its value (translated text)
94: translation += String.Format(" {0}", Convert.ToString(translationLineString.Current))
95: Next
96: 'Remove first blank character
97: If translation.Length > 1 Then
98: translation = translation.Substring(1)
99: End If
100: 'Return translation
101: Return translation
102: End Function
103: 'JavaScript version of google translator https://github.com/matheuss/google-translate-api
104:
105: End Class
And this a pretty simple method of testing class. Main request to testing class - code coverage, ie you must cover all methods in your class. But this test is pretty simple, because code has linear structure without nesting one function to another one. In fact this is low level API.
1: Module Test
2:
3: Dim TS As New FakeTextService
4:
5:
6: Public Sub TestFakeTranslate()
7: Dim GoogleTranslatedPhrase As String = ""
8: Debug.WriteLine(TS.GoogleTranslateText("Активация пользователя"))
9: End Sub
10:
11: Public Sub TestPhraseToName()
12: Debug.WriteLine(TS.PhraseToName("HOLIDAY.RU - User Activation"))
13: End Sub
14:
15: Public Sub TestCache1()
16: Dim OneRusLiteral As String = "Активация пользователя"
17:
18: Dim GoogleTranslatedPhrase As String = ""
19: Dim ResourseName As String = ""
20: If Not TS.TranslatedCacheIsExist(OneRusLiteral, GoogleTranslatedPhrase, ResourseName) Then
21: 'cache service
22: GoogleTranslatedPhrase = "HOLIDAYRUUserctivation"
23: TS.TranslatedCacheAdd(OneRusLiteral, GoogleTranslatedPhrase, GoogleTranslatedPhrase)
24: End If
25: End Sub
Second class Module1 is more sophisticated.
1: Imports System.Resources
2: Imports System.Text.RegularExpressions
3: Imports System.Globalization
4:
5: Public Class Module1
6:
7: Dim TS As New FakeTextService
8:
9: Public Sub Main()
10: Dim AllFileList() As String = {}
11: Dim RelatedResourceFileList As New ArrayList
12: Dim RelatedResourceList As New ArrayList
13: GetWorkableFilename(AllFileList, RelatedResourceFileList, RelatedResourceList)
14:
15: For FileListRowNumber = 0 To AllFileList.Count - 1
16: Dim OneFileName As String = AllFileList(FileListRowNumber) 'здесь рождается новая ссьлка на строку, теперь изменение OneFileName уже не меняет llFileList(FileListRowNumber)
17: If Not OneFileName.StartsWith("'") Then
18: If ProcessOneSourceVBfile(OneFileName, RelatedResourceList(FileListRowNumber), RelatedResourceFileList(FileListRowNumber)) Then
19: 'comment processed file
20: AllFileList(FileListRowNumber) = "'" & OneFileName
21: IO.File.WriteAllLines("E:\Projects\Arenda_5\MyArenda\TextToResourse\TextToResourse\VBfiles.txt", AllFileList)
22: End If
23: End If
24: Next
25:
26: End Sub
27:
28: Public Overridable Sub GetWorkableFilename(ByRef AllFileList() As String, ByRef ResourceFileList As ArrayList, ByRef ResourceList As ArrayList)
29: Console.WriteLine(Console.OutputEncoding.EncodingName)
30: Console.OutputEncoding = Text.Encoding.UTF8
31:
32: AllFileList = IO.File.ReadAllLines("E:\Projects\Arenda_5\MyArenda\TextToResourse\TextToResourse\VBfiles.txt")
33: Dim RelatedResourceFileList As New ArrayList
34: Dim RelatedResourceList As New ArrayList
35:
36: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\ActivateLogin.aspx.vb
37: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\UserInfo1_Mobile.ascx.vb
38: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\SubscriptAttention.ashx
39: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\Old_App_Code\AddAdvertising.vb
40:
41:
42: Array.ForEach(AllFileList, Sub(X)
43: RelatedResourceList.Add(X.Replace("E:\Projects\Arenda_5\MyArenda\ArendaNew\", "").
44: Replace(".ashx", "_ashx").
45: Replace(".aspx.vb", "_aspx").
46: Replace(".ascx.vb", "_ascx").
47: Replace("Old_App_Code\", "Old_App_Code_").Replace(".vb", ""))
48: RelatedResourceFileList.Add("E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\" & RelatedResourceList(RelatedResourceList.Count - 1) & ".ru-RU.resx")
49: End Sub)
50: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx
51: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\UserInfo1_Mobile_ascx.ru-RU.resx
52: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\SubscriptAttention_ashx.ru-RU.resx
53: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\Old_App_Code_AddAdvertising.ru-RU.resx
54:
55: For Each OneFile As String In RelatedResourceFileList
56: If Not My.Computer.FileSystem.FileExists(OneFile) Then
57: Console.WriteLine("FileNotExist " & OneFile)
58: End If
59: Next
60: ResourceFileList = RelatedResourceFileList
61: ResourceList = RelatedResourceList
62: End Sub
63:
64:
65: Public Overridable Function ProcessOneSourceVBfile(OneFileName As String, RelatedResourceName As String, RelatedResourceFile As String) As Boolean
66: Console.OutputEncoding = Text.Encoding.UTF8
67: Dim SourceVBCode As String = IO.File.ReadAllText(OneFileName)
68: Dim Reg1 As New Regex("""(.|\n)*?""")
69: Dim StringLiterals As MatchCollection = Reg1.Matches(SourceVBCode)
70: Console.BackgroundColor = ConsoleColor.Blue
71: Console.WriteLine(OneFileName & ", match=" & StringLiterals.Count)
72: Console.BackgroundColor = ConsoleColor.Black
73: Dim IsGoogleStop As Boolean = False
74:
75: Dim RusLiterals As New ArrayList
76: Dim ReplacedResourceString As New ArrayList
77:
78: For Each OneRusLiteral As Match In StringLiterals
79: Dim HasCyrillic As Boolean = OneRusLiteral.Value.Where(Function(X) IsCyrillic(X)).Any
80: If HasCyrillic Then
81: Do While True
82: Console.Write(OneRusLiteral.Value.ToString & " [y/n]")
83: Dim YNKey As ConsoleKey = Console.ReadKey(False).Key
84: If YNKey = ConsoleKey.Y Then
85:
86: Dim ResourseName As String = ""
87: Dim GoogleTranslatedPhrase As String = ""
88: If Not GetResourceName(OneRusLiteral.Value, ResourseName, GoogleTranslatedPhrase) Then
89: IsGoogleStop = True
90: Exit For
91: Else
92: 'check and add resource
93: AddResourceIfNeeded(RelatedResourceFile, ResourseName, OneRusLiteral.Value)
94: AddResourceIfNeeded(RelatedResourceFile.Replace(".ru-RU", ""), ResourseName, GoogleTranslatedPhrase)
95: RusLiterals.Add(OneRusLiteral.Value)
96: ReplacedResourceString.Add("Resources." & RelatedResourceName & "." & ResourseName)
97: End If
98:
99: Exit Do
100: ElseIf YNKey = ConsoleKey.N Then
101: Exit Do
102: End If
103: Loop
104: Console.WriteLine()
105: End If
106: Next
107: 'replace all Rus literal to regerence with resource
108: ReplaceSourceVBcode(OneFileName, SourceVBCode, RusLiterals, ReplacedResourceString)
109: If IsGoogleStop Then Return False Else Return True
110: End Function
111:
112: Public Function GetResourceName(OneRusLiteral As String, ByRef ResourseName As String, ByRef GoogleTranslatedPhrase As String) As Boolean
113:
114: If Not TS.TranslatedCacheIsExist(OneRusLiteral, GoogleTranslatedPhrase, ResourseName) Then
115: 'cache service
116: GoogleTranslatedPhrase = TS.GoogleTranslateText(OneRusLiteral)
117: If GoogleTranslatedPhrase = "" Then
118: ' translate.googleapis.com site use is very limited. It only allows about 100 requests per one hour
119: Return False
120: End If
121: ResourseName = TS.PhraseToName(GoogleTranslatedPhrase)
122: TS.TranslatedCacheAdd(OneRusLiteral, GoogleTranslatedPhrase, ResourseName)
123: Else
124: ResourseName = TS.PhraseToName(GoogleTranslatedPhrase)
125: End If
126: Return True
127: End Function
128:
129:
130: Public Sub AddResourceIfNeeded(RelatedResourceFile As String, ResourseName As String, TXT As String)
131: 'https://docs.microsoft.com/en-us/dotnet/framework/resources/working-with-resx-files-programmatically
132: If ResourseName = "" Then
133: Throw New Exception("ResourseName Is empty")
134: End If
135: 'The previous resources entries will be overwrited if we write the the same resource file again
136: Dim Resource As New List(Of KeyValuePair(Of String, String))
137: Dim IsResourceExist As Boolean = False
138: Dim ExistResource As ResXResourceSet = New ResXResourceSet(RelatedResourceFile)
139: If ExistResource.GetString(ResourseName) IsNot Nothing Then
140: IsResourceExist = True
141: Else
142: Dim Enums As IDictionaryEnumerator = ExistResource.GetEnumerator
143: While Enums.MoveNext
144: Resource.Add(New KeyValuePair(Of String, String)(Enums.Key, Enums.Value))
145: End While
146: End If
147: ExistResource.Close()
148: If Not IsResourceExist Then
149: Dim ResxFile As ResXResourceWriter = New ResXResourceWriter(RelatedResourceFile)
150: For Each OneRes As KeyValuePair(Of String, String) In Resource
151: ResxFile.AddResource(OneRes.Key, OneRes.Value)
152: Next
153: ResxFile.AddResource(ResourseName, TXT)
154: ResxFile.Generate()
155: ResxFile.Close()
156: End If
157: End Sub
158:
159: Public Overridable Sub ReplaceSourceVBcode(OneFileName As String, SourceVBCode As String, RusLiterals As ArrayList, ReplacedResourceString As ArrayList)
160: For i As Integer = 0 To RusLiterals.Count - 1
161: SourceVBCode = Replace(SourceVBCode, RusLiterals(i), ReplacedResourceString(i))
162: Next
163: IO.File.WriteAllText(OneFileName, SourceVBCode)
164: End Sub
165:
166: Public Function IsCyrillic(c As Char) As Boolean
167: Return AscW(c) >= &H410 And AscW(c) <= &H44F
168: End Function
169:
170: End Class
Low level API is only two method GetWorkableFilename and AddResourceIfNeeded. Other low level API is ReplaceSourceVBcode, but for method is so difficult to testing separately, more simple way is call this method with high level APi and check a result in ASP.NET project. Another low level function is IsCyrillic, but it testing as part of ProcessOneSourceVBfile. Other function is high level of API, it contains execution of cf low level API in the same class and from class FakeTextService.
And below you can see a testing class. See it carefully, to understand how I have isolated nested low level calling function and how I organize function to be called testing method.
1: Module Test
...
27: Public Sub TestCache2()
28: Dim MD As New Module1
29: Dim ResourseName As String = ""
30: Dim GoogleTranslatedPhrase As String = ""
31: MD.GetResourceName("В ОТПУСК.РУ - Активация пользователя", ResourseName, GoogleTranslatedPhrase)
32: MD.GetResourceName("Аренда", ResourseName, GoogleTranslatedPhrase)
33: MD.GetResourceName("Dos атака", ResourseName, GoogleTranslatedPhrase)
34: MD.GetResourceName("Активация пользователя", ResourseName, GoogleTranslatedPhrase)
35: Debug.WriteLine(ResourseName)
36: End Sub
37:
38: Public Sub TestGetWorkableFilename()
39: Dim MD As New FakeProcessOneSourceVBfile
40: Dim AllFileList() As String = {}
41: Dim RelatedResourceFileList As New ArrayList
42: Dim RelatedResourceList As New ArrayList
43: MD.GetWorkableFilename(AllFileList, RelatedResourceFileList, RelatedResourceList)
44: Debug.Print(AllFileList(0))
45: Debug.Print(RelatedResourceFileList(0))
46: Debug.Print(RelatedResourceList(0))
47: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\ActivateLogin.aspx.vb
48: 'E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx
49: 'ActivateLogin_aspx
50: End Sub
51:
52: Public Sub TestMainSub()
53: Dim MD As New FakeGetWorkableFilename
54: MD.Main()
55: End Sub
56:
57: Public Sub TestAddResourceIfNeeded1()
58: Dim MD As New Module1
59: MD.AddResourceIfNeeded("E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx", "RentProperty", "Аренда")
60: MD.AddResourceIfNeeded("E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx", "RentProperty", "Аренда")
61: End Sub
62:
63: Public Sub TestAddResourceIfNeeded2()
64: Dim MD As New Module1
65: MD.AddResourceIfNeeded("E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx", "RentProperty", "Аренда")
66: MD.AddResourceIfNeeded("E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx", "InvalidParametersSpecified", "Указаны неверные параметры")
67: End Sub
68:
69: Public Sub TestReplaceSourceVBcode()
70: Dim MD As New FakeReplaceSourceVBcode
71: MD.ProcessOneSourceVBfile("E:\Projects\Arenda_5\MyArenda\ArendaNew\ActivateLogin.aspx.vb", "ActivateLogin_aspx", "E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx")
72: End Sub
73:
74: Public Sub TestProcessOneSourceVBfile()
75: Dim MD As New Module1
76: MD.ProcessOneSourceVBfile("E:\Projects\Arenda_5\MyArenda\ArendaNew\User.aspx.vb", "User_aspx", "E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\User.aspx.ru-RU.resx")
77: End Sub
78:
79: Public Sub Main()
80: Dim MD As New Module1
81: MD.Main()
82: End Sub
83:
84:
85: End Module
86:
87: Public Class FakeGetWorkableFilename
88: Inherits FakeProcessOneSourceVBfile
89:
90: Public Overloads Overrides Sub GetWorkableFilename(ByRef AllFileList() As String, ByRef ResourceFileList As ArrayList, ByRef ResourceList As ArrayList)
91: Dim AllFile() As String = {"E:\Projects\Arenda_5\MyArenda\ArendaNew\ActivateLogin.aspx.vb"}
92: Dim FileList As New ArrayList
93: FileList.Add("E:\Projects\Arenda_5\MyArenda\ArendaNew\App_GlobalResources\ActivateLogin_aspx.ru-RU.resx")
94: Dim List As New ArrayList
95: List.Add("ActivateLogin_aspx")
96: AllFileList = AllFile
97: ResourceFileList = FileList
98: ResourceList = List
99: End Sub
100:
101: End Class
102:
103: Public Class FakeProcessOneSourceVBfile
104: Inherits FakeReplaceSourceVBcode
105:
106: Public Overloads Overrides Function ProcessOneSourceVBfile(OneFileName As String, RelatedResourceName As String, RelatedResourceFile As String) As Boolean
107: Return True
108: End Function
109:
110: End Class
111:
112: Public Class FakeReplaceSourceVBcode
113: Inherits Module1
114:
115: Public Overloads Overrides Sub ReplaceSourceVBcode(OneFileName As String, SourceVBCode As String, RusLiterals As ArrayList, ReplacedResourceString As ArrayList)
116: For i As Integer = 0 To RusLiterals.Count - 1
117: SourceVBCode = Replace(SourceVBCode, RusLiterals(i), ReplacedResourceString(i))
118: Next
119: Debug.WriteLine(SourceVBCode)
120: End Sub
121:
122: End Class
For testing I permanently, step-by-step changed entry point (function MAIN) form one function to another. This renaming is disadvantage of TDD-testing in the same project, because if you create test in separate project each testing method is calling separately (testing project can contains many methods), but this program is interactive, not a simple class. And write a simulator of console input and output and isolate input/output from other part of code need a time. For enterprise project and continuous delivery separate test project is mandatory job, but for this project this is only dumb waste of time. I want to coverage all function in class Module1 and a I done this for 100% by simplest way.
|