Here is a link to a video of me talking about this topic
If you don’t like the video, here is a lengthy version .
Software can be changed. This is why it is called "software". Its plasticity is stronger than hardware. A great team of engineers should be an amazing asset to a company, writing systems that continue to add value as the business grows.
So why are we so bad at this? How many projects have you heard of that completely failed? Or it becomes "legacy" and must be completely rewritten (rewrites usually fail too!)
How do software systems "fail"? Can't it be modified before it's correct? This is our promise!
Many people choose to build systems in Go because it has made many choices that people hope will make it more legible. [Related recommendations: Go video tutorial]
Compared with my previous Scala careerI describe it as making you have the urge to hang yourself , Go has only 25 keywords and many systems that can be built from the standard library and some other small libraries. The vision is that with Go you can write code and look back on it in 6 months and it will still make sense.
Tools for testing, benchmarking, semantic parsing and loading are top notch compared to most alternatives.
Great standard library.
Strict feedback loop makes compilation very fast
Go has a backwards compatibility commitment. It looks like Go will get generics and other features in the future, but the designers have promised that even Go code you wrote 5 years ago will still build. I've spent a few weeks upgrading my project from Scala 2.8 to 2.10.
Even with all these great properties, we can still make bad systems, so we should. Regardless of whether the language you use is great or not, you should look back at past software engineering. and understand the lessons learned.
In 1974, a smart software engineer named [Manny Lehman](https://en.wikipedia.org/wiki/Manny_Lehman...) wrote Download Lehman Software Evolution Law.
These laws describe the balance between the forces that drive new developments and the forces that hinder progress.
These are the forces we need to understand if we don't want the systems we develop to become legacy and be rewritten over and over again.
The software systems used in real life must constantly change, otherwise they will be eliminated by the general environment
Obviously, a systemmustcontinuously change, otherwise it will become more and more useless, but why is this situation often ignored?
Because Many development teams get a bonus for delivering a project by a specified date, and then they move on to the next project. If the software is "lucky", at least it will be handed over in some form to another group of people to maintain it, but they will certainly not continue to iterate on it.
People are often concerned about choosing a framework to help them "deliver quickly" rather than focusing on system persistence development.
Even if you are a great software engineer, you can still fall victim to not understanding the future needs of your system. As your business changes, the great code you wrote may no longer be relevant.
Lehman was very successful in the 1970s because he gave us another rule worth pondering.
As the system develops, the complexity of the system will continue to increase unless measures are taken to reduce the increase in system complexity.
What he wants to say now is: We cannot let the software team become a pure function factory, just by concentrating more and more functions on the software so that the system can continue to run for a long time.
As our knowledge landscape changes, we must continue to manage the complexity of our systems.
Software development can maintain the plasticity of the software in many aspects, such as:
Developer Authorization
Usually "good" code. Pay attention to the reasonable separation of code, etc.
Communication ability
Architecture
Observability
Deployability
Automated testing
Closed loop
I will focus on refactoring. A common refrain heard from developers on their first day of programming is "we need to refactor this".
This sentence comes from and? How is refactoring different from writing code?
I know that I and many others thought that we were refactoring, but we were wrong.
Martin Fowler describes how people make mistakes
However, "refactoring" is often used in inappropriate places. If someone discusses a system that was down for a few days while they were refactoring, you can be sure they weren't refactoring.
what is that?
When you studied mathematics in school, you probably learned about factorization. Here is a very simple example
Calculate1/2 1/4
To do this, factor the denominator, converting the expression to
2/4 1/4
You can turn it into 3/4
.
We can learn some important lessons from this. When we decompose an expression, we do not change the meaning of the expression. Both are equal to 3/4
, but our job becomes easier by changing 1/2
to 2/4
; it More suitable for our "field".
When you refactor your code, you should try to find a way to make your code easier to understand while "fitting" your current system requirements. The key is that you should not change the original behavior of the code .
The following method uses specific language
Greetingsname
func Hello(name, language string) string { if language == "es" { return "Hola, " + name } if language == "fr" { return "Bonjour, " + name } // 想象一下更多的语言 return "Hello, " + name }
If there are dozens of if
statements it will feel uncomfortable, and we have to use specific ## repeatedly #language goes with
, greeting
name. So let's refactor the code.
func Hello(name, language string) string { return fmt.Sprintf( "%s, %s", greeting(language), name, ) } var greetings = map[string]string { es: "Hola", fr: "Bonjour", //等等... } func greeting(language string) string { greeting, exists := greetings[language] if exists { return greeting } return "Hello" }
two things at the same time. As software engineers, we should learn to split the system into different files/packages/functions/etc. because we know that trying to understand a big chunk of something is difficult.
We should not think about many things at once, because that will make us make mistakes. I've witnessed many refactoring efforts fail because the developers bit off more than they could chew. When I factored with pen and paper in math class, I had to manually check if I had changed the meaning of the expression in my head. When we refactor code, especially on a critical system, how do we know if we've changed functionality? Those who choose not to write tests often rely on manual testing. Unless it is a small project, this will be a huge time-consuming and in the long run will not be conducive to future expansion of the system.In order to refactor safely, you need unit testing because it provides
Hello method like this:
func TestHello(t *testing.T) { got := Hello("Chris", es) want := "Hola, Chris" if got != want { t.Errorf("got %q want %q", got, want) } }
go test and get immediate feedback on whether our refactoring work affects the running of the original program. In fact, it's better to learn to run tests in the editor/IDE.
hinders refactoring.
Ask yourself, how often do you need to change tests when refactoring? I've been involved in many projects over the years that had very high test coverage, but engineers were reluctant to refactor because they thought changing the tests would be laborious. This is contrary to our promise!Two right triangles make a square
We write unit tests around the square to make sure both sides are equal and then we write some tests around the triangle. We want to make sure our triangles are rendered correctly so we assert that the sum of the angles is 180 degrees, we do two tests to check, etc. Test coverage is very important and writing these tests is so easy, why not?
A few weeks later the changing laws hit our system and a new developer made some changes. She now thinks that it would be better to make a square out of two rectangles rather than two triangles.
Two rectangles form a square
He tried this refactoring and got some hints from some failed tests. Did he really break important functionality of the code? She must now delve deeper into the testing of these triangles and try to understand what exactly is going on inside it.
It doesn't actually matter that a square is composed of triangles, but our tests mistakenly elevated the importance of the implementation detail.
When I hear people complain about unit tests, it's usually because the tests are at the wrong level of abstraction. They were all testing implementation details, obsessively observing their collaborators' code, and making lots of mockery.
I believe this problem is due to their misunderstanding of unit testing and their pursuit of metrics (test coverage).
If I'm talking about just testing functionality, shouldn't we just be writing system/black box tests? These types of tests do have a lot of value in validating key user journeys, but they are often expensive to write and slow to run. For this reason, they are not very helpful for refactoring because the feedback loop is slow. Furthermore, compared to unit testing, black box testing does not help much in solving the underlying problem.
So what is the correct abstraction level?
Forget about tests for a moment, it’s better to include self-contained, decoupled “units” in your system, Center on key concepts in your field.
I like to think of these units as simple Lego bricks with consistent APIs that I can combine with other bricks to build larger systems. Inside these APIs, there may be many things (types, functions, etc.) that collaborate to make them work as needed.
For example, if you use Go to develop a banking system, you should have an "account" package. It will provide an API that does not leak implementation details and is easy to integrate.
If your unit follows these properties, you can write unit tests against their public API. By definition, these tests can only test useful functionality. With these units in place, we are free to refactor whenever necessary, and testing will not hinder us in most cases.
Yes. Unit tests are for what I describe as "units". They never only target one class/function/whatever.
We’ve already covered it
Refactoring
Unit testing
Unit design
What we can see is that these aspects of software design are complementary to each other.
Provides signals for our unit tests. If we have to check manually, then we need more tests. If the test fails, then our test is at the wrong level of abstraction (or has no value and should be removed)
helps us deal with complexity within and between units.
Provides safety protection for refactoring.
Verify and document the functionality of our units.
Easy to write meaningful unit tests.
Easy to refactor.
Is there a process that helps us continually refactor our code to manage complexity and keep the system scalable?
Some people may over-design and waste a lot of time trying to create a "perfect" scalable system in advance because of Lehman's ideas on how to make software constantly change. As a result, But nothing was accomplished.
In the bad old days of software, a team of analysts would spend 6 months writing a requirements document, a team of architects would spend 6 months designing, and a few years later the entire project would fail.
I said the old days were bad, but they still are!
Agile development teaches us that we need to work iteratively, starting small and continually improving the software so that we can quickly Get feedback on the design of the software and how it works with actual users; TDD enforces this approach.
TDD addresses the laws Lehman talks about and other historically hard-to-learn lessons by encouraging a development approach that is constantly refactoring and delivering iteratively.
Write a small test for a small function
Check whether the test fails, And there are obvious errors (red)
Write the minimum code to make the test pass (green)
Refactor
Repeat the above steps
As your proficiency level increases, this will become a natural way of working for you, and your work efficiency will also increase. Getting higher and higher
You start to hope that it doesn't take too long for your small test unit to complete the entire test, because if you see that your program has been in a non-"green" state, that indicates that you may have encountered A little trouble.
With these test feedbacks, you can easily ensure the stability of some small application functions.
The advantage of the software is that I can change it as needed. Over time, due to some unpredictable reasons, most software needs to make corresponding changes according to needs; but don't overwork and over-design at the beginning, because the future is too difficult to predict.
So in order to meet the above needs, we need to maintain the scalability of our software. Otherwise, the situation will become very bad when our software needs to be refactored and upgraded.
A good unit test can help you refactor your project quickly and happily.
Writing good unit tests is a design problem. You must think carefully to structure your code so that each of your unit tests fits together as interestingly as putting together Lego blocks. , successfully completed the testing of the entire project.
Test-driven development (TDD) can help and prompt you to iteratively develop a well-designed software. Using it as technical support will have a great impact on your future work. Big help.
Original address: https://quii.gitbook.io/learn-go-with-tests/meta/why
Translation address: https: //learnku.com/go/t/34095
For more programming-related knowledge, please visit: Programming Video! !
The above is the detailed content of Why unit testing? How to conduct the test?. For more information, please follow other related articles on the PHP Chinese website!