TDD is now mainstream but a lot people don't know or don't remember what is its purpose. TDD is about software design not testing or catching bug. TDD helps developers to shape and create software with "good" design, what is a "good" design is something that we will discuss in the topic.
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
TDD is not only unit testing
1. TDD is not about
testing
Bad name for good technique ...
2. GPad
Born to be a developer with an interest in distributed system.
I have developed with many languages like C++, C#, js and ruby. I had fallen in
love with functional programming, especially with elixir, erlang.
● Twitter: https://twitter.com/gpad619
● Github: https://github.com/gpad/
● Medium: https://medium.com/@gpad
CTO & founder of coders51
4. What is TDD - History
Wikipedia - TDD
Wikipedia - Extreme Programming
Quora - Why does Kent Beck rediscover TDD
http://wiki.c2.com/?TestingFramework
Wikipedia - C2 System
5. What is TDD - History
The C3 project started in 1993 [...]. Smalltalk development was initiated in 1994. [...] In
1996 Kent Beck was hired [...]; at this point the system had not printed a single
paycheck.
In March 1996 the development team estimated the system would be ready to go into
production around one year later.
In 1997 the development team adopted a way of working which is now formalized as
Extreme Programming.
The one-year delivery target was nearly achieved, with actual delivery being a couple of
months late;
6. What is TDD - History
It was developed by Grady Booch, Ivar Jacobson
and James Rumbaugh at Rational Software in 1994–
1995, with further development led by them
through 1996.
In 1997 UML was adopted as a standard by the
Object Management Group (OMG) [...].
In 2005 UML was also published by the International Organization for
Standardization (ISO) as an approved ISO standard. Since then the standard
has been periodically revised to cover the latest revision of UML.
8. What is TDD - History
From Wikipedia:
Test-driven development (TDD) is a software development process that relies on the
repetition of a very short development cycle: requirements are turned into very specific
test cases, then the software is improved to pass the new tests, only.
This is opposed to software development that allows software to be added that is not
proven to meet requirements.
American software engineer Kent Beck, who is credited with having developed or
"rediscovered" the technique, stated in 2003 that TDD encourages simple designs and
inspires confidence.
10. What is TDD
1. Add a test
2. Run all tests and see if the new test
fails (RED)
3. Write the code (Only to make the
test pass!!!)
4. Run tests
5. Refactor code
6. Repeat
12. Why TDD
Less bugs
I like it …
I feel more comfortable …
…
Helps to create a “GOOD” design
13. Why TDD
TDD changes the point of view.
Forces the developer to think about the “behaviour” of the code.
Talks to the developer showing what are the “difficult points” of the code.
If the tests talk, we should listen them … (often we ignore them).
14. Listen the test
TEST(MailComposerTest, test1)
{
User gino{"gino", "gino@gino.it", true};
User pino{"pino", "pino@pino.it", false};
auto userRepository = UserRepository::create();
userRepository->CreateUser(gino);
userRepository->CreateUser(pino);
MailComposer composer;
composer.SendEmail("gino@gino.it", "body one");
composer.SendEmail("pino@pino.it", "body two");
auto emailsSent = ExternalLibrary::TestEmailServer::GetEmailsSent();
ASSERT_EQ(emailsSent.size(), 1);
ExternalLibrary::Email expected{"gino@gino.it", GlobalSettings::GetFromAddress(), "body one"};
ASSERT_EQ(*emailsSent.begin(), expected);
}
15. Listen the test
class MailComposer
{
public:
void SendEmail(std::string email, std::string body)
{
using namespace ExternalLibrary;
auto userRepository = UserRepository::create();
auto user = userRepository->GetUserByEmail(email);
if (!user.acceptEmail())
return;
EmailServer::GetInstance()->SendEmail(Email{
GlobalSettings::GetFromAddress(),
user.email(),
body});
}
};
16. Listen the test
It isn’t a tool problem.
It’s a design problem …
What is telling (screaming) the test?
17. Listen the test
TEST(MailComposerTest, test1)
{
User gino{"gino", "gino@gino.it", true};
User pino{"pino", "pino@pino.it", false};
auto userRepository = UserRepository::create();
userRepository->CreateUser(gino);
userRepository->CreateUser(pino);
MailComposer composer;
composer.SendEmail("gino@gino.it", "body one");
composer.SendEmail("pino@pino.it", "body two");
auto emailsSent = ExternalLibrary::TestEmailServer::GetEmailsSent();
ASSERT_EQ(emailsSent.size(), 1);
ExternalLibrary::Email expected{"gino@gino.it", GlobalSettings::GetFromAddress(), "body one"};
ASSERT_EQ(*emailsSent.begin(), expected);
}
The name doesn’t mean anything
We are changing global value
Singleton ?!?
18. Listen the test
TEST(MailComposerTest, SendEmailOnlyToUserThatAcceptEmail)
{
User gino{"gino", "gino@gino.it", true};
User pino{"pino", "pino@pino.it", false};
auto userRepository = UserRepository::create();
userRepository->CreateUser(gino);
userRepository->CreateUser(pino);
MailComposer composer(
userRepository,
ExternalLibrary::EmailServer::GetInstance());
composer.SendEmail("gino@gino.it", "body one");
composer.SendEmail("pino@pino.it", "body two");
auto emailsSent = ExternalLibrary::TestEmailServer::GetEmailsSent();
ASSERT_EQ(emailsSent.size(), 1);
ExternalLibrary::Email expected{"gino@gino.it", GlobalSettings::GetFromAddress(), "body one"};
ASSERT_EQ(*emailsSent.begin(), expected);
}
Change name!!!
Passing repository as parameter ...
Passing Singleton as parameter
21. Type of tests
End to End tests - Tests that work on the entire stack.
Integration Test - Tests that work on some parts of the application.
Unit Test - Tests that work on a single “module/function”.
Why is it so important to know which type of test we are writing?
Because we get different feedbacks from different types of test.
25. End To End Test or Acceptance Test
This type of test exercises the entire stack of the application.
It remains RED until the feature is completed.
Don’t write too much E2E.
They are slow and fragile.
Where is the design?
27. How application is done (or should be)
Try to create a E2E test that interacts
with system from the external.
If it’s “impossible” try to move a little
inside skipping the adapter.
29. Outside-In
We start writing an End to End Test that explains the feature that we want to
create.
We add some unit tests to shape the behaviour of the object that we want to
create.
We add some integration tests to verify that some parts works well together.
30. Inside-Out
We start writing some units test to shape the behaviour of the objects that we
want to create.
We add some integration tests to verify that the parts works well together.
We add some End to End Tests that explain the feature that we want to create.
31. My 2 cents
I prefer to use Outside-In when I have to create a new feature.
Outside-In requires more experienced developers/teams.
I use Inside-Out when I want to introduce the TDD in a new team.
I use Inside-Out when I have to add a feature and I know which objects I have
to change.
33. When to use Mock?
When we want to isolate one part from another.
Classic example HTTP connections.
We have to make some HTTP calls and manipulate the results in some ways.
How can we do it?
34. When to use Mock?
TEST(TwitterClientTest, RetriveBioFromUser)
{
std::string clientId = "cleintId";
std::string clientSecret = "clientSecret";
NiceMock<MockHttpClient> mockHttpClient;
EXPECT_CALL(mockHttpClient, Get("https://twitter.com/gpad619"))
.WillOnce(Return("gpad_bio"));
TwitterClient twitter(clientId, clientSecret, &mockHttpClient);
auto bio = twitter.GetBioOf("gpad619");
EXPECT_EQ(bio, "gpad_bio");
}
35. When to use Mock?
We are at the boundaries of our
system and we can use the mock to
shape the behavior between the
CORE and the adapter that talks with
the external system.
It’s the CORE that choose the shape
of the data and how the functions
should be done.
36. When to use Mock?
TEST(AggregationTest, SaveTheSumWhenThresholdIsReached)
{
NiceMock<MockPointRepository> mockPointRepository;
EXPECT_CALL(mockPointRepository, Store(4));
Aggregation aggregation(4, &mockPointRepository);
aggregation.Add(2);
aggregation.Add(2);
}
37. When to use Mock? - Time
TEST(AggregationTest, SaveTheEventWhenThresholdIsReached)
{
auto now = std::chrono::system_clock::now();
NiceMock<MockPointRepository> mockPointRepository;
EXPECT_CALL(mockPointRepository, StoreEvent("Threshold Reached", now));
Aggregation aggregation(4, &mockPointRepository);
aggregation.Add(2);
aggregation.Add(2);
}
39. When to use Mock? - Time
class Aggregation
{
int _sum;
int _threshold;
PointRepository *_pointRepository;
public:
void Add(int value)
{
_sum += value;
if (_sum >= _threshold)
{
_pointRepository->Store(_sum);
_pointRepository->StoreEvent("Threshold Reached", std::chrono::system_clock::now());
}
}
};
This is a dependency!!!
40. When to use Mock? - Time
TEST(AggregationTest, SaveTheEventWhenThresholdIsReached)
{
auto now = std::chrono::system_clock::now();
NiceMock<MockPointRepository> mockPointRepository;
EXPECT_CALL(mockPointRepository, StoreEvent("Threshold Reached", now));
NiceMock<MockClock> mockClock;
ON_CALL(mockClock, Now()).WillByDefault(Return(now));
Aggregation aggregation(4, &mockPointRepository, &mockClock);
aggregation.Add(2);
aggregation.Add(2);
}
41. When to use Mock? - Time
class Aggregation
{
...;
public:
Aggregation(int threshold, PointRepository *pointReposiotry, Clock *clock) ...
void Add(int value)
{
_sum += value;
if (_sum >= _threshold)
{
_pointRepository->Store(_sum);
_pointRepository->StoreEvent("Threshold Reached", _clock->Now());
}
}
};
42. When to use Mock? - Time
How to manage a periodic task?
Divide et impera.
43. When to use Mock? - Time
TEST(Scheduler, ExecuteTaskAfterSomeAmountOfTime)
{
Scheduler scheduler;
bool executed = false;
scheduler.ExecuteAfter(
std::chrono::milliseconds(500),
[&]() { executed = true; });
EXPECT_THAT(executed, WillBe(true, std::chrono::milliseconds(1000)));
}
44. When to use Mock? - Time
TEST(Scheduler, DontExecuteTaskImmediatly)
{
Scheduler scheduler;
bool executed = false;
scheduler.ExecuteAfter(
std::chrono::milliseconds(500),
[&]() { executed = true; });
EXPECT_FALSE(executed);
}
45. WHERE to use Mock?
Use the mock at the external of
CORE/Domain (adapter).
Isolate the CORE from the infrastructure
not because we want to switch from PG
to Mongo but because that libraries are
not under our control.
46. When to use Mock? - Interaction between objects
TEST(CartTest, RetrieveDiscoutForUserWhenAddItem)
{
User user("gino", "g@g.it", true);
NiceMock<MockTrackingSystem> mockTrackingSystem;
NiceMock<MockDiscountEngine> mockDiscountEngine;
EXPECT_CALL(mockDiscountEngine, GetDiscountFor(user))
.WillRepeatedly(Return(0.1));
Cart cart(user, &mockDiscountEngine, &mockTrackingSystem);
cart.AddItem(Product(ProductId::New(), "Product", Money(10, "EUR")));
EXPECT_EQ(cart.GetTotal(), Money(9, "EUR"));
}
47. When to use Mock? - Interaction between objects
TEST(CartTest, CallTrackingSystemWhenRemoveItemFromCart)
{
User user("gino", "g@g.it", true);
Product product(ProductId::New(), "Product", Money(10, "EUR"));
NiceMock<MockTrackingSystem> mockTrackingSystem;
NiceMock<MockDiscountEngine> mockDiscountEngine;
EXPECT_CALL(mockTrackingSystem, ProductRemoved(product));
Cart cart(user, &mockDiscountEngine, &mockTrackingSystem);
cart.AddItem(product);
cart.RemoveItem(product);
}
48. WHERE to use Mock?
We can use the mock when we want to
define how the object talk to each
other.
We can define the protocol that the
object should use to talk to each other.
49. When not to use Mock?
Don’t use mock on Object that don’t own.
TEST(MailComposerTest, TestMailComposerMock)
{
User gino{"gino", "gino@gino.it", true};
User pino{"pino", "pino@pino.it", false};
auto userRepository = UserRepository::create();
userRepository->CreateUser(gino);
userRepository->CreateUser(pino);
NiceMock<MockExternalEmailServer> mockExternalEmailServer;
MailComposer composer(userRepository, &mockExternalEmailServer);
...
}
50. When not to use Mock?
Don’t mock value objects!!!
TEST(MailComposerTest, TestMailComposerMockUser)
{
NiceMock<MockUser> gino();
NiceMock<MockUser> pino();
EXPECT_CALL(gino, acceptEmail()).WillRepeatedly(Return(true));
EXPECT_CALL(pino, acceptEmail()).WillRepeatedly(Return(true));
auto userRepository = UserRepository::create();
MailComposer composer(userRepository, ExternalLibrary::EmailServer::GetInstance());
composer.SendEmail(gino, "body one");
composer.SendEmail(pino, "body two");
}
51. TDD - Mix Everything together
What is the first test to do?
We could start from outside with an E2E test to enter inside our application.
Or we could start inside our application adding some unit tests to some
objects.
Create at least one E2E for every User Story.
Don’t create too much E2E they are slow and fragile.
52. TDD - Mix Everything together
TEST_F(AsLoggedUser, IWantSeeMyCurrentCart)
{
auto cart = CreateCartFor(_currentUser);
auto total = GetMainWindow()
->GetCartWindowFor(_currentUser)
->GetTotal();
EXPECT_EQ(total, cart.GetTotal());
}
53. TDD - Mix Everything together
TEST_F(AsLoggedUser, IWantPayMyCurrentCart)
{
auto cart = CreateCartFor(_currentUser);
auto paymentInfo = CreatePaymentInfoFor(_currentUser);
auto paymentResult = GetMainWindow()
->GetPaymentWindowFor(_currentUser)
->Pay();
EXPECT_EQ(paymentResult, PaymentResult::Successful);
}
54. TDD - Mix Everything together
The E2E remains RED until all the cycle is completed.
After that we have written the E2E we go inside the CORE and start to create
some unit tests.
56. TDD - Mix Everything together
After we have written the unit tests for the CORE we could move to the
boundaries where we should write tests for the adapter parts.
The test for storage part should be written using the DB. IMHO they are more
integration than unit.
58. TDD - Mix Everything together
Writing these tests BEFORE the implementation we are doing DESIGN.
We are shaping the production code.
The code became more “composable”.
It’s more clear where are side effects (I/O, Time).
It’s more clear what are the different parts of our applications.
59. Recap
TDD is not a Silver Bullet.
TDD doesn’t give us a “good” design if we are not able to do it.
TDD can help us to find some issues in our design.
Listen the test, often they are screaming in pain ...