Skip to content

Instantly share code, notes, and snippets.

@zahra-ove
Last active November 12, 2024 07:13
Show Gist options
  • Save zahra-ove/07f367d11f09b66234236dcb142806b8 to your computer and use it in GitHub Desktop.
Save zahra-ove/07f367d11f09b66234236dcb142806b8 to your computer and use it in GitHub Desktop.

important notes from book: "Unit testing Principles, Practices and Patterns by Viladimir Khorikov"

  1. AAA (Arrange, Act, Assert) or Given-When-Then

  2. A unit in unit testing is a unit of behavior, not a unit of code. A single unit of behavior can exhibit multiple outcomes, and it’s fine to evaluate them all in one test. (p. 69)

  3. Having that said, you need to watch out for assertion sections that grow too large: it could be a sign of a missing abstraction in the production code. (p. 69)

  4. Note that most unit tests don’t need teardown. (p. 69)

  5. a modification of one test should not affect other tests. (p. 74) ... tests should run in isolation from each other.

  6. you need to avoid introducing shared state in test classes.

  7. important note about "method naming in test classes" : (p. 79)

Method under test in the test’s name Don’t include the name of the SUT’s method in the test’s name. Remember, you don’t test code, you test application behavior. Therefore, it doesn’t matter what the name of the method under test is. As I mentioned previously, the SUT is just an entry point: a means to invoke a behavior. You can decide to rename the method under test to, say, IsDeliveryCorrect, and it will have no effect on the SUT’s behavior. On the other hand, if you follow the original naming convention, you’ll have to rename the test. This once again shows that targeting code instead of behavior couples tests to that code’s implementation details, which negatively affects the test suite’s maintainability. More on this issue in chapter 5. The only exception to this guideline is when you work on utility code. Such code doesn’t contain business logic—its behavior doesn’t go much beyond simple auxiliary functionality and thus doesn’t mean anything to business people. It’s fine to use the SUT’s method names there.

  1. As you can see, there’s a trade-off between the amount of test code and the readability of that code. As a rule of thumb, keep both positive and negative test cases together in a single method only when it’s self-evident from the input parameters which case stands for what. Otherwise, extract the positive test cases. And if the behavior is too complicated, don’t use the parameterized tests at all. Represent each negative and positive test case with its own test method. (p. 82)

  2. All unit tests should follow the AAA pattern: arrange, act, assert. If a test has multiple arrange, act, or assert sections, that’s a sign that the test verifies multiple units of behavior at once. If this test is meant to be a unit test, split it into several tests—one per each action.

  3. A good unit test has the following four attributes:

  • Protection against regressions
  • Resistance to refactoring
  • Fast feedback
  • Maintainability
  1. a regression is a software bug. (p. 90)

  2. To maximize the metric of protection against regressions, the test needs to aim at exercising as much code as possible.

  3. This situation is called a false positive. A false positive is a false alarm. It’s a result indicating that the test fails, although in reality, the functionality it covers works as intended.

  4. To evaluate how well a test scores on the metric of resisting to refactoring, you need to look at how many false positives the test generates. The fewer, the better. (p. 70)

  5. the goal of unit testing is to enable sustainable project growth.

  6. The number of false positives a test produces is directly related to the way the test is structured. The more the test is coupled to the implementation details of the system under test (SUT), the more false alarms it generates.

  7. The best way to structure a test is to make it tell a story about the problem domain.

  8. As I mentioned earlier, the only way to avoid brittleness in tests and increase their resistance to refactoring is to decouple them from the SUT’s implementation details (p. 74)

  9. This accuracy is what the first two pillars of a good unit test are all about. Protection against regressions and resistance to refactoring aim at maximizing the accuracy of the test suite. The accuracy metric itself consists of two components:

  • How good the test is at indicating the presence of bugs (lack of false negatives, the sphere of protection against regressions)
  • How good the test is at indicating the absence of bugs (lack of false positives, the sphere of resistance to refactoring). (p. 99)
  1. In search of an ideal test Here are the four attributes of a good unit test once again:
  • Protection against regressions
  • Resistance to refactoring
  • Fast feedback
  • Maintainability These four attributes, when multiplied together, determine the value of a test. And by multiplied, I mean in a mathematical sense; that is, if a test gets zero in one of the attributes, its value turns to zero as well:

Value estimate = [0..1] * [0..1] * [0..1] * [0..1]

TIP. In order to be valuable, the test needs to score at least something in all four categories.

  1. In reality, though, resistance to refactoring is non-negotiable. You should aim at gaining as much of it as you can, provided that your tests remain reasonably quick and you don’t resort to the exclusive use of end-to-end tests. The trade-off, then, comes down to the choice between how good your tests are at pointing out bugs and how fast they do that: that is, between protection against regressions and fast feedback. You can view this choice as a slider that can be freely moved between protection against regressions and fast feedback. The more you gain in one attribute, the more you lose on the other.

img


  1. test pyramid:

img

note: The height of the layer is a measure of how close these tests are to emulating the end user’s behavior. End-to-end tests are at the top—they are the closest to imitating the user experience.

note: The width of the pyramid layers refers to the prevalence of a particular type of test in the suite. The wider the layer, the greater the test count.


  1. The exact mix between types of tests will be different for each team and project. But in general, it should retain the pyramid shape: end-to-end tests should be the minority; unit tests, the majority; and integration tests somewhere in the middle.

  2. There are exceptions to the Test Pyramid. For example, if all your application does is basic create, read, update, and delete (CRUD) operations with very few business rules or any other complexity, your test “pyramid” will most likely look like a rectangle with an equal number of unit and integration tests and no end-to-end tests.

  3. The other well-known test automation concept is black-box versus white-box testing. (p. 89)


chapter 5

  1. indeed, mocks often result in fragile tests—tests that lack the metric of resistance to refactoring. (p. 92)

  2. There’s a deep and almost inevitable connection between mocks and test fragility. In the next several sections, I will gradually lay down the foundation for you to see why that connection exists. You will also learn how to use mocks so that they don’t compromise a test’s resistance to refactoring.

  3. The major use of test doubles is to facilitate testing; they are passed to the system under test instead of real dependencies, which could be hard to set up or maintain.

  4. According to Gerard Meszaros, there are five variations of test doubles: dummy, stub, spy, mock, and fake.1 Such a variety can look intimidating, but in reality, they can all be grouped together into just two types: mocks and stubs. (p. 93)

imag2

  1. difference between Mock and Stub:

imag2

  1. Notice the difference between mocks and stubs (aside from outcoming versus incoming interactions). Mocks help to emulate and examine interactions between the SUT and its dependencies, while stubs only help to emulate those interactions. This is an important distinction. You will see why shortly. (p. 94)

  2. The notions of mocks and stubs tie to the command query separation (CQS) principle. The CQS principle states that every method should be either a command or a query, but not both. As shown in figure 5.3, commands are methods that produce side effects and don’t return any value (return void). Examples of side effects include mutating an object’s state, changing a file in the file system, and so on. Queries are the opposite of that—they are side-effect free and return a value. (p. 97)

  3. To follow this principle, be sure that if a method produces a side effect, that method’s return type is void. And if the method returns a value, it must stay side-effect free.

  4. CQS: command query separation

img3

  1. test fragility corresponds to the second attribute of a good unit test: resistance to refactoring. (As a reminder, the four attributes are protection against regressions, resistance to refactoring, fast feedback, and maintainability.)

  2. Maintaining a well-designed API relates to the notion of encapsulation.

  3. As you can see, there’s an intrinsic connection between good unit tests and a welldesigned API. By making all implementation details private, you leave your tests no choice other than to verify the code’s observable behavior, which automatically improves their resistance to refactoring.

  4. The combination of the application services layer and the domain layer forms a hexagon, which itself represents your application.

  5. The separation of concerns between the application services layer and the domain layer means that the former knows about the latter, but the opposite is not true. The domain layer should be fully isolated from the external world.

  6. There are two types of communications in a typical application: intra-system and intersystem. Intra-system communications are communications between classes inside your application. Inter-system communications are when your application talks to other applications

  7. The use of mocks is beneficial when verifying the communication pattern between your system and external applications. Conversely, using mocks to verify communications between classes inside your system results in tests that couple to implementation details and therefore fall short of the resistance-to-refactoring metric. (p. 111)

  8. The ability for tests to run in parallel, sequentially, and in any order is called test isolation. (p. 115)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment