Skip to main content

Articles

2023

Golang Tutorial: Unit Testing with Mocking

·3513 words·17 mins· loading · loading
Unit testing has always been my thing, almost like a hobby. There was a time when I was obsessed with it, and I made sure that all my projects had at least 90% unit test coverage. You can probably imagine how much time it can take to make such a significant change in the codebase. However, the result was worth it because I rarely encountered bugs related to business logic. Most of the issues were related to integration problems with other services or databases. Adding new business rules was a breeze because there were already tests in place to cover all the cases from before. The key was to ensure that these tests remained successful in the end. Sometimes, I didn’t even need to check the entire running service; having the new and old unit tests pass was sufficient. Once, while working on a personal project, I had to write unit tests to cover numerous Go structs and functions—more than 100 in total. It consumed my entire weekend, and late on a Sunday night, before heading out on a business trip the next day, I set an alarm clock to wake me up. I had hardly slept that night; it was one of those restless nights when you dream but are also aware of yourself and your surroundings. My brain was active the entire time, and in my dreams, I kept writing unit tests for my alarm clock. To my surprise, each time I executed a unit test in my dream, the alarm rang. It continued ringing throughout the night. And yes, I almost forgot to mention, for two years, we had zero bugs in production. The application continued to fetch all the data and send all the emails every Monday. I don’t even remember my Gitlab password anymore. Unit Testing and Mocking (in general) # In Martin Fowler’s article, we can identify two types of unit tests: Sociable unit tests, where we test a unit while it relies on other objects in conjunction with it. For example, if we want to test the UserController, we would test it along with the UserRepository, which communicates with the database. Solitary unit tests, where we test a unit in complete isolation. In this scenario, we would test the UserController, which interacts with a controlled, mocked UserRepository. With mocking, we can specify how it behaves without involving a database.

Golang Tutorial: Generics

·3344 words·16 mins· loading · loading
How often do we encounter significant changes in our preferred programming language? Some languages undergo frequent updates, while others remain traditional and stable. Go falls into the latter category, known for its consistency. “This is not the Go way!” is a phrase that often comes to mind. Most Go releases have focused on refining its existing principles. However, a major shift is on the horizon. The Go team has announced that Generics in Go are becoming a reality, moving beyond mere discussion and into implementation. Brace yourselves, a revolution is coming. What are Generics? # Generics allow us to parameterize types when defining interfaces, functions, and structs. Generics is not a new concept. It has been used since the first version of Ada, through templates in C++, to its modern implementations in Java and C#. To illustrate without delving into complex definitions, let’s examine a practical example. Instead of having multiple Max or Min functions like this: Without Generics func MaxInt(a, b int) int { // some code } func MaxFloat64(a, b float64) float64 { // some code } func MaxByte(a, b byte) byte { // some code } we can declare now only one method, like this: With Generics func Max[T constraints.Ordered](a, b T) T { // some code } Wait, what just happened? Instead of defining a method for each type in Go, we utilized Generics. We used a generic type, parameter T, as an argument for the method. With this minor adjustment, we can support all orderable types. The parameter T can represent any type that satisfies the Ordered constraint (we will discuss constraints later). Initially, we need to specify what kind of type T is. Next, we determine where we want to use this parameterized type. In this case, we’ve specified that both input arguments and the output should be of type T. If we execute the method by defining T as an integer, then everything here will be an integer: Execute Generic Function func main() { fmt.Println(Max[int](1, 2)) } // // this code behaves exactly like method: // Max(a, b int) int And it doesn’t stop there. We can provide as many parameterized types as we need and assign them to different input and output arguments as desired: Execute some complex Generic Function func Do[R any, S any, T any](a R, b S) T { // some code } func main() { fmt.Println(Do[int, uint, float64](1, 2)) } // // this code behaves exactly like method: // Do(a int, b uint) float64 Here we have three parameters: R, S, and T. As we can see from the any constraint (which behaves like interface{}), those types can be, well, anything. So, up to this point, we should have a clear understanding of what generics are and how we use them in Go. Let’s now focus on more exciting consequences.

Practical SOLID in Golang: Dependency Inversion Principle

·2306 words·11 mins· loading · loading
Learning a new programming language is often a straightforward process. I often hear: “The first programming language you learn in a year. The second one in a month. The third one in a week, and then each next one in a day.” Saying that is an exaggeration, but it is not too distant from the truth in some cases. For example, jumping to a language relatively similar to the previous one, like Java and C#, can be a straightforward process. But sometimes, switching is tricky, even when we switch from one Object-Oriented language to another. Many features influence such transitions, like strong or weak types, if a language has interfaces, abstract classes, or classes at all. Some of those difficulties we experience immediately after switching, and we adopt a new approach. But some issues we experience later, during unit testing, for example. And then, we learn why The Dependency Inversion Principle is essential, especially in Go. When we do not respect The Dependency Inversion # High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. Above is the definition of DIP as presented by Uncle Bob in his paper. There are also more details inside his blog. So, how can we understand this, especially in the context of Go? First, we should accept Abstraction as an object-oriented programming concept. We use this concept to expose essential behaviors and hide the details of their implementation. Second, what are high and low-level modules? In the context of Go, high-level modules are software components used at the top of the application, such as code used for presentation. It can also be code close to the top level, like code for business logic or some use-case components. It is essential to understand it as a layer that provides real business value to our application. On the other hand, low-level software components are mostly small code pieces that support the higher level. They hide technical details about different infrastructural integrations. For example, this could be a struct that contains the logic for retrieving data from the database, sending an SQS message, fetching a value from Redis, or sending an HTTP request to an external API. So, what does it look like when we break The Dependency Inversion Principle, and our high-level component depends on one low-level component?

Practical SOLID in Golang: Interface Segregation Principle

·1722 words·9 mins· loading · loading
When beginners embark on their programming journey, the initial focus is typically on algorithms and adapting to a new way of thinking. After some time, they delve into Object-Oriented Programming (OOP). If this transition is delayed, it can be challenging to shift from a functional programming mindset. However, eventually, they embrace the use of objects and incorporate them into their code where necessary, sometimes even where they’re not needed. As they learn about abstractions and strive to make their code more reusable, they may overgeneralize, resulting in abstractions applied everywhere, which can hinder future development. At some point, they come to realize the importance of setting boundaries for excessive generalization. Fortunately, The Interface Segregation Principle has already provided a guideline for this, representing the “I” in the word SOLID. When we do not respect The Interface Segregation # Maintain small interfaces to prevent users from relying on unnecessary features. Uncle Bob introduced this principle, and you can find more details about it on his blog. This principle clearly states its requirement, perhaps better than any other SOLID principle. Its straightforward advice to keep interfaces as small as possible should not be interpreted as merely advocating one-method interfaces. Instead, we should consider the cohesion of features that an interface encompasses. Let’s analyze the code below: User interface type User interface { AddToShoppingCart(product Product) IsLoggedIn() bool Pay(money Money) error HasPremium() bool HasDiscountFor(product Product) bool // // some additional methods // } Let’s assume we want to create an application for shopping. One approach is to define an interface User, as demonstrated in the code example. This interface includes various features that a user can have. On our platform, a User can add a Product to the ShoppingCart, make a purchase, and receive discounts on specific Products. However, the challenge is that only specific types of Users can perform all of these actions. Guest struct type Guest struct { cart ShoppingCart // // some additional fields // } func (g *Guest) AddToShoppingCart(product Product) { g.cart.Add(product) } func (g *Guest) IsLoggedIn() bool { return false } func (g *Guest) Pay(Money) error { return errors.New("user is not logged in") } func (g *Guest) HasPremium() bool { return false } func (g *Guest) HasDiscountFor(Product) bool { return false } We have implemented this interface with three structs. The first one is the Guest struct, representing a user who is not logged in but can still add a Product to the ShoppingCart. The second implementation is the NormalCustomer, which can do everything a Guest can, plus make a purchase. The third implementation is the PremiumCustomer, which can use all features of our system.

Practical SOLID in Golang: Liskov Substitution Principle

·1880 words·9 mins· loading · loading
I’m not really a fan of reading. Often, when I do read, I find myself losing track of the text’s topic for the past few minutes. Many times, I’ll go through an entire chapter without really grasping what it was all about in the end. It can be frustrating when I’m trying to focus on the content, but I keep realizing I need to backtrack. That’s when I turn to various types of media to learn about a topic. The first time I encountered this reading issue was with the SOLID principle, specifically the Liskov Substitution Principle. Its definition was (and still is) too complicated for my taste, especially in its formal format. As you can guess, LSP represents the letter “L” in the word SOLID. It’s not difficult to understand, although a less mathematical definition would be appreciated. When we do not respect The Liskov Substitution # The first time we encountered this principle was in 1988, thanks to Barbara Liskov. Later, Uncle Bob shared his perspective on this topic in a paper and eventually included it as one of the SOLID principles. Let’s take a look at what it says: Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T. Well, good luck with that definition. No, seriously, what kind of definition is this? Even as I write this article, I’m still struggling to fully grasp this definition, despite my fundamental understanding of LSP. Let’s give it another shot: If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program. Okay, this is a bit clearer now. If ObjectA is an instance of ClassA, and ObjectB is an instance of ClassB, and ClassB is a subtype of ClassA – if we use ObjectB instead of ObjectA somewhere in the code, the application’s functionality must not break. We’re talking about classes and inheritance here, two concepts that aren’t prominent in Go. However, we can still apply this principle using interfaces and polymorphism. Wrong implementation of Update method type User struct { ID uuid.UUID // // some fields // } type UserRepository interface { Update(ctx context.Context, user User) error } type DBUserRepository struct { db *gorm.DB } func (r *DBUserRepository) Update(ctx context.Context, user User) error { return r.db.WithContext(ctx).Delete(user).Error } In this code example, we can see one that’s quite absurd and far from best practices. Instead of updating the User in the database, as the Update method claims, it actually deletes it. But that’s precisely the point here. We have an interface, UserRepository, followed by a struct, DBUserRepository. While this struct technically implements the interface, it completely diverges from what the interface is supposed to do. In fact, it breaks the functionality of the interface rather than fulfilling its expectations. This highlights the essence of the Liskov Substitution Principle (LSP) in Go: a struct must not violate the intended behavior of the interface.

Practical SOLID in Golang: Open/Closed Principle

·1369 words·7 mins· loading · loading
Many different approaches and principles can lead to long-term improvements in our code. Some of them are well-known in the software development community, while others remain somewhat under the radar. In my opinion, this is the case with The Open/Closed Principle, represented by the letter O in the word SOLID. In my experience, only those genuinely interested in SOLID principles tend to understand what this principle means. We may have applied this principle without even realizing it in some instances, such as when working with the Strategy pattern. However, the Strategy pattern is just one application of the Open/Closed Principle. In this article, we will delve into the full purpose of this principle, with all examples provided in Go. When we do not respect the Open/Closed Principle # You should be able to extend the behavior of a system without having to modify that system. The requirement for the Open/Closed Principle, as seen above, was provided by Uncle Bob in his blog. I prefer this way of defining The Open/Closed Principle because it highlights its full brilliance. At first glance, it may seem like an absurd requirement. Seriously, how can we extend something without modifying it? I mean, is it possible to change something without changing it? By examining the code example below, we can see what it means for certain structures not to adhere to this principle and the potential consequences. The bad Authentication Service type AuthenticationService struct { // // some fields // } func (s *AuthenticationService) Authenticate(ctx *gin.Context) (*User, error) { switch ctx.GetString("authType") { case "bearer": return c.authenticateWithBearerToken(ctx.Request.Header) case "basic": return c.authenticateWithBasicAuth(ctx.Request.Header) case "applicationKey": return c.authenticateWithApplicationKey(ctx.Query("applicationKey")) } return nil, errors.New("unrecognized authentication type") } func (s *AuthenticationService) authenticateWithApplicationKey(key string) (*User, error) { // // authenticate User from Application Key // } func (s *AuthenticationService) authenticateWithBasicAuth(h http.Header) (*User, error) { // // authenticate User from Basic Auth // } func (s *AuthenticationService) authenticateWithBearerToken(h http.Header) (*User, error) { // // validate JWT token from the request header // } The example presents a single struct, AuthenticationService. Its purpose is to authenticate a User from the web application’s Context, supported by the Gin package. Here, we have the main method, Authenticate, which checks for specific authentication type associated with the data within the Context. How User is retrieved from the Context may vary based on whether the User uses a bearer JWT token, basic authentication, or an application key.

Practical SOLID in Golang: Single Responsibility Principle

·2013 words·10 mins· loading · loading
There aren’t too many opportunities for a breakthrough in software development. They usually arise from either rewiring our logic after initial misunderstandings or filling in gaps in our knowledge. I appreciate that feeling of deeper understanding. It can happen during a coding session, while reading a book or an online article, or even while sitting on a bus. An internal voice follows, saying, “Ah, yes, that’s how it works.” Suddenly, all past mistakes seem to have a logical reason, and future requirements take shape. I experienced such a breakthrough with the SOLID principles, which were first introduced in a document by Uncle Bob and later expounded upon in his book, “Clean Architecture.” In this article, I intend to embark on a journey through all the SOLID principles, providing examples in Go. The first principle on the list, representing the letter ‘S’ in SOLID, is the Single Responsibility Principle. When we do not respect Single Responsibility # The Single Responsibility Principle (SRP) asserts that each software module should serve a single, specific purpose that could lead to change. The sentence above comes directly from Uncle Bob himself. Initially, its application was linked to modules and the practice of segregating responsibilities based on the organization’s daily tasks. Nowadays, SRP has a broader scope, influencing various aspects of software development. We can apply its principles to classes, functions, modules, and naturally, in Go, even to structs. Some Frankenstein of EmailService type EmailService struct { db *gorm.DB smtpHost string smtpPassword string smtpPort int } func NewEmailService(db *gorm.DB, smtpHost string, smtpPassword string, smtpPort int) *EmailService { return &EmailService{ db: db, smtpHost: smtpHost, smtpPassword: smtpPassword, smtpPort: smtpPort, } } func (s *EmailService) Send(from string, to string, subject string, message string) error { email := EmailGorm{ From: from, To: to, Subject: subject, Message: message, } err := s.db.Create(&email).Error if err != nil { log.Println(err) return err } auth := smtp.PlainAuth("", from, s.smtpPassword, s.smtpHost) server := fmt.Sprintf("%s:%d", s.smtpHost, s.smtpPort) err = smtp.SendMail(server, auth, from, []string{to}, []byte(message)) if err != nil { log.Println(err) return err } return nil } Let’s analyze the code block above. In this code, we have a struct called EmailService, which contains only one method, Send. This service is intended for sending emails. Although it may seem okay at first glance, upon closer inspection, we realize that this code violates the Single Responsibility Principle (SRP) in several ways.

Practical DDD in Golang: Specification

·1596 words·8 mins· loading · loading
There are not many code structures that bring me joy whenever I need to write them. The first time I implemented such code was with a lightweight ORM in Go, back when we didn’t have one. However, I used ORM for many years, and at some point, when you rely on ORM, using QueryBuilder becomes inevitable. Here, you may notice terms like “predicates”, and that’s where we can find the Specification pattern. It’s hard to find any pattern we use as Specification, yet we do not hear its name. I think the only thing harder is to write an application without using this pattern. The Specification has many applications. We can use it for querying, creation, or validation. We may provide a unique code that can do all this work or provide different implementations for different use cases. For Validation # The first use case for the Specification pattern is validation. Typically, we validate data in forms, but this is at the presentation level. Sometimes, we perform validation during creation, such as for Value Objects. In the context of the domain layer, we can use Specifications to validate the states of Entities and filter them from a collection. So, validation at the domain layer has a broader meaning than for user inputs. Base Product Specification type Product struct { ID uuid.UUID Material MaterialType IsDeliverable bool Quantity int } type ProductSpecification interface { IsValid(product Product) bool } A simple Product Specification type HasAtLeast struct { pieces int } func NewHasAtLeast(pieces int) ProductSpecification { return HasAtLeast{ pieces: pieces, } } func (h HasAtLeast) IsValid(product Product) bool { return product.Quantity >= h.pieces } In the example above, there is an interface called ProductSpecification. It defines only one method, IsValid, which expects instances of Product and returns a boolean value as a result if the Product passes validation rules. A simple implementation of this interface is HasAtLeast, which verifies the minimum quantity of the Product. Function as a Product Specification type FunctionSpecification func(product Product) bool func (fs FunctionSpecification) IsValid(product Product) bool { return fs(product) } func IsPlastic(product Product) bool { return product.Material == Plastic } func IsDeliverable(product Product) bool { return product.IsDeliverable } More interesting validators are two functions, IsPlastic and IsDeliverable. We can wrap those functions with a specific type, FunctionSpecification. This type embeds a function with the same signature as the two mentioned. Besides that, it provides methods that respect the ProductSpecification interface. This example is a nice feature of Go, where we can define a function as a type and attach a method to it so that it can implicitly implement some interface. In this case, it exposes the method IsValid, which executes the embedded function.

Practical DDD in Golang: Repository

·2108 words·10 mins· loading · loading
Today, it is hard to imagine writing an application without accessing some form of storage at runtime. This includes not only writing application code but also deployment scripts, which often need to access configuration files, which are also a type of storage in a sense. When developing applications to solve real-world business problems, connecting to databases, external APIs, caching systems, or other forms of storage is practically unavoidable. It’s no surprise, then, that Domain-Driven Design (DDD) includes patterns like the Repository pattern to address these needs. While DDD didn’t invent the Repository pattern, it added more clarity and context to its usage. The Anti-Corruption Layer # Domain-Driven Design (DDD) is a principle that can be applied to various aspects of software development and in different parts of a software system. However, its primary focus is on the domain layer, which is where our core business logic resides. While the Repository pattern is responsible for handling technical details related to external data storage and doesn’t inherently belong to the business logic, there are situations where we need to access the Repository from within the domain layer. Since the domain layer is typically isolated from other layers and doesn’t directly communicate with them, we define the Repository within the domain layer, but we define it as an interface. This interface serves as an abstraction that allows us to interact with external data storage without tightly coupling the domain layer to specific technical details or implementations. A Simple Repository example type Customer struct { ID uuid.UUID // // some fields // } type Customers []Customer type CustomerRepository interface { GetCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error) SearchCustomers(ctx context.Context, specification CustomerSpecification) (Customers, int, error) SaveCustomer(ctx context.Context, customer Customer) (*Customer, error) UpdateCustomer(ctx context.Context, customer Customer) (*Customer, error) DeleteCustomer(ctx context.Context, ID uuid.UUID) (*Customer, error) } The interface that defines method signatures within our domain layer is referred to as a “Contract.” In the example provided, we have a simple Contract interface that specifies CRUD (Create, Read, Update, Delete) methods. By defining the Repository as this interface, we can use it throughout the domain layer. The Repository interface always expects and returns our Entities, such as Customer and Customers (collections with specific methods attached to them, as defined in Go). It’s important to note that the Entity Customer doesn’t contain any information about the underlying storage type, such as Go tags for defining JSON structures, Gorm columns, or anything of that sort. This kind of low-level storage configuration is typically handled in the infrastructure layer.

Practical DDD in Golang: Factory

·1023 words·5 mins· loading · loading
When I wrote the title of this article, I was trying to remember the first design pattern I had learned from “The Gang of Four”. I think it was one of the following: Factory Method, Singleton, or Decorator. I am sure that other software engineers have a similar story. When they started learning design patterns, either Factory Method or Abstract Factory was one of the first three they encountered. Today, any derivative of the Factory pattern is essential in Domain-Driven Design, and its purpose remains the same, even after many decades. Complex Creations # We use the Factory pattern for any complex object creation or to isolate the creation process from other business logic. Having a dedicated place in the code for such scenarios makes it much easier to test separately. In most cases, when I provide a Factory, it is part of the domain layer, allowing me to use it throughout the application. Below, you can see a simple example of a Factory. Simple example type Loan struct { ID uuid.UUID // // some fields // } type LoanFactory interface { CreateShortTermLoan(specification LoanSpecification) Loan CreateLongTermLoan(specification LoanSpecification) Loan } The Factory pattern goes hand-in-hand with the Specification pattern. Here, we have a small example with LoanFactory, LoanSpecification, and Loan. LoanFactory represents the Factory pattern in DDD, and more specifically, the Factory Method. It is responsible for creating and returning new instances of Loan that can vary depending on the payment period. Variations # As mentioned, we can implement the Factory pattern in many different ways. The most common form, at least for me, is the Factory Method. In this case, we provide some creational methods to our Factory struct. Loan Entity const ( LongTerm = iota ShortTerm ) type Loan struct { ID uuid.UUID Type int BankAccountID uuid.UUID Amount Money RequiredLifeInsurance bool } Loan Factory type LoanFactory struct{} func (f *LoanFactory) CreateShortTermLoan(bankAccountID uuid.UUID, amount Money) Loan { return Loan{ Type: ShortTerm, BankAccountID: bankAccountID, Amount: amount, } } func (f *LoanFactory) CreateLongTermLoan(bankAccountID uuid.UUID, amount Money) Loan { return Loan{ Type: LongTerm, BankAccountID: bankAccountID, Amount: amount, RequiredLifeInsurance: true, } } In the code snippet from above, LoanFactory is now a concrete implementation of the Factory Method. It provides two methods for creating instances of the Loan Entity. In this case, we create the same object, but it can have differences depending on whether the loan is long-term or short-term. The distinctions between the two cases can be even more complex, and each additional complexity is a new reason for the existence of this pattern.