Ensuring code quality and performance.
full lessons here👇:
https://programmerscareer.com/golang-basic-skill/
Generated by AI, there may be errors, for reference only
Topic 1.1: Introduction to Testing in Go
Testing your code is fundamental to ensuring its quality and correctness. Tests validate that your code behaves as expected and makes it less prone to errors. If you work in a team, it guarantees that modifications by others don’t break existing functionality.
Go has a built-in package called testing which provides support for automated tests in your Go code. Go’s philosophy encourages well-tested code, and the testing package reflects this, providing easy-to-use features for you to incorporate testing into your coding practice.
In Go, each test is associated with a test function, which follows these rules:
- It must be in a file that ends with
_test.go
. - The test function must start with the word
Test
. - The test function takes one argument only i.e.,
t *testing.T
.
Here’s an example of what a simple test might look like:
1 | package stringutil |
In this example, TestReverse
is the testing function. It’s testing the Reverse
function of our stringutil
package.
Topic 1.2: Understanding Unit Tests
To ensure the reliability of your code, you should test its smallest testable components, referred to as “units”. This style of testing is known as “unit testing”.
Each unit test is an independent test case that verifies the functionality of a specific part of your code. In Go, unit tests are typically created using the testing package, as explained previously.
When we create unit tests, we should follow these guidelines:
- Each unit test should be independent and able to run separately from other tests.
- Tests should be simple, clear, and easy to understand.
- The test should clearly show what it’s testing and what are the expected results.
Here’s a basic example using the strconv.Itoa
function:
1 | package gorilla |
In this example, TestItoa
is our unit test. We’re testing the Itoa
function of the strconv
package. If strconv.Itoa(454545)
doesn’t return the string “454545”, the test fails, and an error message gets displayed.
Topic 1.3: Table-Driven Tests
Testing different cases often requires writing multiple tests with nearly identical test logic but different input values. This can result in a lot of redundant code. Table-driven tests allow you to test different input values for your function using just a single test.
Table-driven tests use a single test function that loops over a table of test cases. The table is an array consisting of struct values, where each struct represents one test case.
Let’s have a look at a table-driven test:
1 | package stringutil |
In this example, we’re creating multiple test cases for the function Reverse
. The input and expected values for all test cases are defined inside the tests
variable. These cases run inside the for loop, where the Reverse
function is called with the input value and checked against the expected output.
If the function fails on any test case, it will print an error message with the input it failed on, the actual output, and the expected output.
This makes it easier to detect which test case failed and why, making debugging easier.
Topic: 1.4 Mocking and Interfaces
Testing complex applications often requires dealing with external services, databases, and APIs, which, under normal circumstances, run independently of our codebase. During testing, these services can have unpredictable behavior or even become unavailable. That’s when mockingcomes in.
Mocking is the technique of replacing parts of your software — typically those that have external dependencies — with dummy implementations, for testing purposes.
Go provides an elegant way of dealing with this using interfaces. A Go interface makes it easy to substitute the real dependencies with mock objects during testing.
Here’s a simple example. Let’s consider the following code:
1 | type Downloader interface { |
In this example, FileDownloader
is our actual implementation we’d use to download a file. The function processFile
takes an argument of type Downloader
, an interface that FileDownloader
implements.
Now, if we want to test the processFile
function without having to hit the outside website, we’d need to create a mock implementation of the Downloader
interface. We’ll call it MockDownloader
.
1 | type MockDownloader struct { |
In the Test_processFile
test function, we’re creating an instance of MockDownloader
and passing it to the processFile
function. This way, the function is tested independently of the external dependency.
Topic: 1.5 Testing Coverage
Test coverage in Go is a measurement of how much of your code is covered by tests. It’s a handy metric that can give you an idea of the extent of your testing.
To calculate test coverage in Go, we use the built-in go test
tool with the -cover
flag.
1 | go test -cover |
This command, when run in your package directory, will perform tests and return a statement like the one below:
1 | PASS |
This percentage tells you how much of your code is “covered” by your tests.
If you want more detailed coverage information, you can use the -coverprofile
flag, along with specifying an output file:
1 | go test -coverprofile=coverage.out |
After you’ve done this, you can analyze the coverage with the go tool cover -html
command, which will produce a detailed, color-coded HTML page:
1 | go tool cover -html=coverage.out |
This will open up a browser window with your code, showing which parts of your code are covered by tests and which aren’t. Lines covered by tests are green and lines without coverage are in red.
Understanding your testing coverage can help you create more comprehensive tests and ensure your code works as expected.
Topic 1.6: Introduction to Benchmarking
While testing helps us ensure that our programs are functioning correctly, it’s equally important to know how efficiently they’re running. This is where benchmarking comes into play.
Benchmarking is the technique of measuring the performance of your code. You typically use it to find out how much time your function takes to execute or how much memory it consumes. It’s used to identify bottlenecks in your code and to find areas of improvement.
In Go, you may write benchmarks similarly to tests, but with a slight variation in the function signature. Benchmark functions start with the word Benchmark
followed by the name of the function you’re about to benchmark.
Let’s consider you have the following function that you’d like to benchmark:
1 | package stringutils |
To benchmark the Reverse
function, you could write a benchmark function like this:
1 | package stringutils |
A benchmark function receives a pointer to testing.B
. This contains the number of iterations that the benchmark loop should make, among other fields. It automatically adjusts the value of b.N
to achieve an optimal balance between precision and running time.
To run the benchmarks, you can use the go test
command with the -bench
flag:
1 | go test -bench=. |
The .
after the bench
flag indicates that Go should run all benchmarks in the current directory.
By analyzing the results of your benchmarks, you could make adjustments to your functions to improve their performance.
Topic 1.7: Benchmarking Different Parts of the Code
Sometimes your entire function might not be the bottleneck, and the slowdown could be due to just a small part of it. In such cases, benchmarking the entire function won’t be as helpful. That’s where benchmarking specific parts of the code come in handy.
However, benchmarking specific parts of a function can be a bit tricky in Go, since the b.N
loop controls the number of times the function executes. If you want to benchmark a particular part of that function, you’ll have to ensure that part gets executed b.N
number of times.
Here’s a simple way to do it:
1 | package stringutils |
In this modified benchmark, only half of the data
string is reversed at a time, simulating the process of benchmarking a particular part of a function.
When you run this benchmark, you can compare its output with the output of the earlier benchmark where the entire string was reversed, giving insight into where the potential bottleneck in the function might be.
Keep in mind, though, that benchmarking itself adds an overhead, so it’s best not to have too many benchmarks in your actual program. They are tools for your development and testing environment to help you write efficient programs.
Topic 1.8: Analysis and Parsing Benchmark Results
Once we have benchmarked our Go code, it’s important to understand the results. Parsing benchmark results involves understanding what each of the output numbers signifies and how changes in those numbers relate to the changes you made in your code.
When you run a benchmark in Go, here is a typical output you would get:
1 | BenchmarkReverse-4 1000000 1238 ns/op |
Here’s what each part of this output means:
BenchmarkReverse-4
: This is the name of the testing function. The-4
indicates that the benchmark was run using 4 CPUs.1000000
: This is the number of times the operation was performed during the benchmark test.1238 ns/op
: This is the cost of the operation. In this case, it’s saying that our Reverse function takes 1238 nanoseconds on average to perform one operation.
Armed with this knowledge, you can now compare the benchmark outputs before and after code changes to check whether the changes have improved the performance or made it worse. You can also compare the performances of different functions or different parts of a function to locate any bottlenecks.
Topic: 1.9 Overview and Importance of Good Practices
As software engineers, one of the things we should always strive for is writing clean, efficient, and manageable code. All the techniques we’ve learned above, like testing, benchmarking, and analyzing the results, are part of a broader agenda that emphasizes good practices in coding.
Why are these practices important? Here are a few reasons:
- Readability and Maintainability: Well-tested and benchmarked code is likely clean and manageable. This makes it easier for others (and for your future self) to read and maintain your code.
- Performance: Benchmarking helps you find bottlenecks in your program. By continually optimizing these areas, you ensure that your code performs efficiently.
- Reliability: By testing your code and fixing bugs, you increase the reliability of your program.
- Collaboration: Good coding practices like writing descriptive comments, using meaningful variable names, and organizing code logically, all make collaboration with other developers easier.
Remember, the aim is not just to make your code work. It’s to create code that can stand the test of time, be understood by others, perform efficiently, and be reliable.
Topic: 1.10 Review, Practice, and Assessments
In this lesson block, we have covered several topics, mainly focusing on testing and benchmarking your Go code. A quick recap:
- Writing Tests in Go: We learned about the importance of testing, how to write, run, and benchmark tests in Go.
- Performance Metrics: Understanding how to interpret the test results and performance metrics.
- Benchmarking: We explored how to benchmark specific parts of the function to dig deeper and find out where the bottleneck might be.
- Analysis and parsing benchmark results: We learned about making sense of benchmark results and drawing insights from them to optimize the code.
- Importance of Good Practices: Finally, we discussed the importance of good coding practices, including testing, benchmarking, writing descriptive comments, using meaningful variable names, and logically organizing the code.
Now, it’s time to put your knowledge to the test. Let’s have a quick assessment:
- Write a small description, in your own words, on how to benchmark Go functions.
- What information does a typical Go benchmark output provide? Give an example and describe each part of the output.
- Why is it important to follow good coding practices, and what are some of these practices?
中文文章: https://programmerscareer.com/zh-cn/go-basic-13/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: If you choose to repost or use this article, please cite the original source.
Comments