I am a software QA engineer working on transitioning to software engineering. And while I won't bore you too much on the journey so far, I have had some experiences that could be useful or at least shed some light on my overall experience.
And while this won’t be (yet) another long-winded, "cry me a river" post about the struggles of breaking into the field it will be honest, transparent and raw.
Now, about that code I am raving about... it is my own.
Before we start dissecting it or dropping some 💩 emojis (yes I am aware it's most likely 💩...) let's talk about why.
As a QA engineer, a key tool is a well-defined test plan. These living documents are the tools that help make the lives of many SEs' a nightmare – or, at the very least, they help all of us ship and share somewhat functional features that won't blow up the first time an end user actually puts their eager hands on it. In my daily workflows, I found myself manually building the structure and format of these test plans to then enter all of the information pertinent to them.
Like many of you, when faced with a problem, I want to solve it. Of course the idea is to increase efficiency, or at least avoid tedious manual tasks – but its more than that...
Enter the Test Plan Builder, a CLI tool, to help me focus on the actual content of the test plan and spend less time structuring the template.
I jumped on the Go bandwagon in 2024 and have thoroughly enjoyed the learning experience. So, it seemed like the natural choice for this project.
Demo of the "current" final product:
Diving Into the Code
First, I applaud you for making it this far! Now that we're all here, it's time to take a detour to ...jk! Let's get into the implementation details! How fun!
Disclaimer: I am not adhering to any methodology/philosophy on test plan structure.
I built this tool to match my own approach to planning and discovery for the user stories I validate with my test plans. While this may not be a one size-fits-all solution, it should provide a good starting point.
Defining the Structure of the Template
type TestPlanTemplate struct {
Name string
TicketDetails
RequiredValidation string
Description string
SavePath string
WriteToFile bool
}
type TicketDetails struct {
UserStory string
UserStoryLink string
UserStoryComponents string
AutomationNeeded bool
PM string
Dev string
QA string
}
For simplicity's sake, and to avoid cognitive overhead with a giant struct, I defined a TicketDetails
struct that houses the information specific to the user story my test plan is tied to, and embedded it into a TestPlanTemplate
struct the overall structure of the test plan itself.
This approach improves readability and simplifies field access. Embedding TicketDetails
sets me up to access ticket details directly through the TestPlanTemplate
instance.
As a final note, all field and struct names are capitalized for future exportability.
The Methods and Helpful Functions
- "Almost A Factory" Really A Constructor in Other Languages
func NewTestPlanBuild() *TestPlanTemplate {
return &TestPlanTemplate{}
}
This "Almost a factory" function is really a constructor in other languages. I could have easily just used GO's built in constructor t := TestPlanTemplate{}
, but I went this route for various reasons.
First, not all the necessary information is available, and I don't want to restrict the instantiation process.
Second, there's only one version of TestPlanTemplate
object ahem I mean struct that will be instantiated. Therefore, no complex logic is required to justify a full factory implementation 'right now'.
Finally, given the potential size of the TestPlanTemplate
struct, I chose this approach to return a pointer instead of GO's default behavior of passing by value. This reduces the number of TestPlanTemplate
struct copies.
- My Take On a Stringer() Method
func (tp TestPlanTemplate) MDString() string {
title := "# " + tp.Name + "\n\n"
link := fmt.Sprintf("**User Story Link:**\t[%s](%s)\n\n", tp.UserStory, tp.UserStoryLink)
table, _ := tableBuilder(tp)
components := fmt.Sprintf("**User Story Components:**\t%s\n\n", tp.UserStoryComponents)
auto := fmt.Sprintf("**Requires Automation:**\t%t\n\n", tp.AutomationNeeded)
description := fmt.Sprintf("## Description:\n%s\n\n", tp.Description)
testplan := "## Test Validation\n\n"
return title + link + table + components + auto + "***\n" + description + "***\n" + testplan
}
I took inspiration from the Stringer workflow in the fmt
package.
While this method doesn't implement the Stringer interface, it uses a similar approach to generate a Markdown representation of the TestPlanTemplate
. Which come in handy in the future.
For those unfamiliar with the Stringer interface, it's defined in the fmt
package. Any type that implements a String() string
method fulfills this interface.
When you pass an instance of a type that implements the Stringer interface to a fmt
function (like fmt.Println
or fmt.Printf
with the %s
or %v
format specifiers), Go automatically calls its String()
method to get the it's string representation. Otherwise, it uses default formatting.
For more information, see A Tour of Go - Stringers
CLI Forms and Charm to the Rescue
Initially I considered using bufio
and os.Stdin
(both part of the standard library) for individual prompts to gather the TestPlanTemplate
data. However, this approach would have been monotonous for the user. Frankly, it's not very aesthetically pleasing. I know I mentioned I built this for myself, but I did take into account the user experience if others decided to use this tool as well.
After researching alternatives, I discovered Charm's Huh library. This library facilitates building and rendering visually appealing forms in the terminal, making it ideal for transforming my basic CLI app into a more engaging TUI.
Using Huh
To keep this concise, I'll summarize the Huh form implementation rather than including the full code snippet.
for full implementation: BuildForm
I defined a BuildForm(tp *TestPlanTemplate) *huh.Form
function which takes in a pointer to a TestPlanTemplate Struct as an argument.
Within the function, I instantiate a new form, define the form inputs (using Huh's API), bind them to their respective TestPlanTemplate
fields, and return a pointer to the form.
For more on huh Forms charm.sh/HUH
Putting It All Together
With a defined structure for the TestPlanTemplate
, a data-collecting form, and a Markdown representation method, we can now put it all together.
In our main.go file, we begin by instantiating a TestPlanTemplate
struct and a huh.Form
.
Then, we execute the form's Run
method. We won't delve into the implementation details here, but the Run
method uses Charm's Bubble Tea library. Bubble Tea generates a model (the form's state) and manages that state. For more information, see the Bubble Tea Documentation
Once we have a completed TestPlanTemplate
, we call the BuildTemplateFromString which takes a pointer to the test plan.
A helper function checks if the savePath field is empty. If it is, the function uses user.Current()
to get the user's home directory, appends /desktop and the filename, and returns the resulting path. If the savePath
field is not empty, the function simply appends the filename to the existing path and returns.
Now that we have the file path I use os.WriteFile(saveFilePath, []byte(tp.MDString()), 0666)
to create and write the file.
I've chosen these permissions to allow read/write access for all users by default (though this might need adjustments depending on specific security requirements).
And now we have a Test Plan Markdown file saved to the disk! Let's go!
What's Next
Now, I'll be dog fooding it. I'll share with my colleagues to gather feedback and make further tweaks. Am I a scientist? No. But is there value in testing, testing, testing. You bet. What’s most important though? Feedback. Even Einstein turned to others to get input.
Some things I considered when designing this tool:
- Implement the ability to build different types of test plans based on the user story type leveraging my constructor function and a new Interface.
- Integrate with jira or other User Story and Test Case Management applications via API's for direct writing.
- Enhance the form's dynamic capabilities and add a preview function to visualize the final output
Finally, if you've made it this far, thank you for sticking with me! This has been my first fully-fleshed-out CLI APP/TUI and my first technical post. I appreciate your time! Writing about this project has been an awesome learning experience.
Now, its my turn to welcome your thoughts and input. Please feel free to leave comments (and 💩 emojis!). Or if you think it's helpful, take the tool for a spin!
Let's ride.
Top comments (0)