Consider Lisa, a developer. A smart one. But lazy.
So lazy that she wants to reduce at minimum the time spent to talk to other developers explaining his code.
She wants to just write her code and move to the next ticket. Sure, she likes to talk to other developers about new technologies, frameworks and patterns. But when it comes to her code, she doesn't want to explain why she added that if-else code or why the function she wrote checks the if a parameter is null. She enjoys discussing why she chose a certain design pattern or used a certain library to solve a particular issue. Those are the interesting and important things to take care when sharing knowledge with the team.
But the small details, gotchas and edge cases you consider when coding do not belong there. They are important of course. But when you write and read the code. So the code should be the keeper of this knowledge.
Requirements and specification documents? Too generic and high level. Sometimes there are things in the code that are not present in the requirements because they are a pure result of the implementation.
Documentation in the code does not help. It gets old pretty fast and you always forget to update. Nobody reads it anyway.
If the code is the keeper of this knowledge, the code should reply these questions. And when the code doesn't because it is not clear enough, only one thing can.
A test represents a part of the knowledge. It might come from the requirements or it might be new knowledge the developer acquired during the implementation. All these pieces of knowledge compose your product. And you want to make sure they don't get lost.
Once a test is written, it stands as a guard. It tells every new developer the story of why the code is like that. It shouts whenever someone dares to change something that should have not be changed.
It is an entity on its own which does the job for you.
Be lazy. Be smart. Write tests.