Open CamilYed opened 1 year ago
Groovy's @Builder
doesn't create a real builder, it is more like Lombok's @Accessors(chained=true)
. Another way to go could be @With
.
You mean sth like this?
@AllArgsConstructor
@NoArgsConstructor
public class Order {
@Default @With private Long orderId = 1L;
private Customer customer = aCustomer().build();
private List<OrderItem> orderItems = Lists.of(anOrderItem().build());
@Default @With private Double discountRate = 10d;
@Default @With private String couponCode = "xaxadxa";
Order withCustomer(Customer.CustomerBuilder customer) {
this.customer = customer.build();
return this;
}
Order withOrderItems(OrderItem.OrderItemBuilder... items) {
this.items = Arrays.stream(items).map(OrderItem.OrderItemBuilder::build).toList();
return this;
}
}
Yes, but I think @Accessors(chained=true)
would be more similar to Groovy's @Builder
.
It's not as powerful as Lombok's @Builder
, e.g. it's missing a @Singular
feature.
However, I must admit I don't really see the problem with Lombok's current builder implementation. For me, the additional builder()
/build()
methods add clarity, because they follow the well-established builder idiom, and not the Groovy style (which is not really common).
I want to have Test Data Builder no common builder - that's a difference. In my tests I want to get rid off some technical concepts - one of them is the builder method.
Try to change your perspective for a while and try to treat your some tests like live documentation. So in my tests I want to have business language, and try to hide some technical implementation details.
From business perspective:
thereIs(anOrder().withAmount(eur(40)))
vs.
thereIs(anOrder().withAmount(eur(40)).build())
A builder without build()
method is impossible because you need some trigger to create the actual object. The only way you can drop the method is using @Accessors(chain = true, prefix = "with")
or @With(er)
. The former will modify the object you passed and the later will create a new object after each method call.
You can combine @With
with an object mother to handle the default values and you are done. There is no need to use @Builder
if you don't want to create a builder.
@Rawi01 Are you sure that I have to use object mother when I use @With
?
@AllArgsConstructor
@NoArgsConstructor
public class Order {
@Default @With private Long orderId = 1L;
private Customer customer = aCustomer().build();
private List<OrderItem> orderItems = Lists.of(anOrderItem().build());
@Default @With private Double discountRate = 10d;
@Default @With private String couponCode = "xaxadxa";
Order withCustomer(Customer.CustomerBuilder customer) {
this.customer = customer.build();
return this;
}
Order withOrderItems(OrderItem.OrderItemBuilder... items) {
this.items = Arrays.stream(items).map(OrderItem.OrderItemBuilder::build).toList();
return this;
}
}
Unfortunately id doest not work I can not mix these annotation.
It should works.
@AllArgsConstructor
@NoArgsConstructor
public class Order {
@With private Long orderId = 1L;
private Customer customer = aCustomer().build();
private List<OrderItem> orderItems = Lists.of(anOrderItem().build());
@With private Double discountRate = 10d;
@With private String couponCode = "xaxadxa";
Order withCustomer(Customer.CustomerBuilder customer) {
this.customer = customer.build();
return this;
}
Order withOrderItems(OrderItem.OrderItemBuilder... items) {
this.items = Arrays.stream(items).map(OrderItem.OrderItemBuilder::build).toList();
return this;
}
}
You only need a object mother if you want to add default values for your objects in your tests. You can also add your test data as default values in your class but I would not design it like that.
You marked this issue as feature request so please add a detailed description what lombok should generate for a given set of annotations (before and after snippet). If you need general coding support please use something like StackOverflow, the forum or ask ChatGPT.
You only need a object mother if you want to add default values for your objects in your tests. You can also add your test data as default values in your class but I would not design it like that.
In my case this class is under test source set, so I don't see any problem.
You marked this issue as feature request so please add a detailed description what lombok should generate for a given set of annotations (before and after snippet). If you need general coding support please use something like StackOverflow, the forum or ask ChatGPT.
Ok I will try give some examples
@CamilYed hi, I know this isn't really an answer to your question if you like to achieve it solely with Java, but if you still happen to use a mix like Java for prod code and Groovy/Spock for tests, I've got an even nicer way of achieving the Test Data Builder pattern with a better SNR.
For a different reason but I have posted here a simple example of how to incorporate Groovy custom DSL based on a Java Lombok generated builder to have clean prod code with no default and all the defaults set inside of the Test Data Builder (to which I refer there as test fixture DSL) and then how it's used on the test side in a very readable manner (well, at least unless it doesn't have a setterPrefix
set, as then it may not be so readable and not even work at all 😓
https://github.com/projectlombok/lombok/issues/3159#issuecomment-2129888213
Describe the feature
The Problem
Nat Pryce once described such a pattern as Test Data Builder. When I once wrote an article on readable tests by example, I used Groovy instead of Java, which comes with the Spock framework.
Why I used Groovy is because it has Builder annotation, which allows me to easily create a Test Data Builder with default field values, plus what is important I can get rid of the build method when constructing objects.
How it's solved in Groovy
But as they say show me the code.
As You cen see I can add my custom method to this builder what is really important!
With this approach we don't have to carry the word “builder” around. See the code below:
How it's solved in Java by Lombok Builder annotation
Now let's move on to Java, here you have to do a lot of tinkering using lombok, which is not satisfactory to me and my colleagues. Let me attach a snippet of the article: How to Create a Test Data Builder. Arho Huttunen
🌶️ Reduce Boilerplate With Lombok While the test data builder pattern provides a lot of benefits, there is also one major drawback. That is, we end up writing a lot of boilerplate code.
To tackle this problem with boilerplate, we can take advantage of the Lombok project. We can get rid of the default constructor, getters and automatically create a builder class by annotating the class with Lombok @Data and @Builder annotations.
Lombok automatically creates a builder class and a factory method inside the class.
Unfortunately, for collections, you now have to pass a collection as the builder argument. Luckily, by annotating a collection field with @Singular, Lombok will generate methods for single items in the collection.
We can now add items to a collection by calling the method multiple times.
We have still lost a little in the readability, at least in my opinion. We have to carry the word “builder” around.
However, we can configure Lombok to generate a different name for the factory method and use static imports. We can also prefix the setter methods.
Now our builder looks a lot more like the custom builder.
There is still one problem, though. This code now suffers from the same problem as our custom builder when we try to vary our data. In our custom builder, we added a but() method to deal with this.
Luckily, Lombok allows us to configure a toBuilder() method.
The difference here is that the toBuilder() is called on a constructed object, not on a builder.
Now Lombok builder is pretty close to the expressiveness of our custom builder.
The obvious benefit of using Lombok is that we get rid of a lot of boilerplate code. However, our code is also a little noisier because we have to write, e.g.:
instead of:
There is one more issue, though. There are no safe default values for our fields. We could add default values in our production code, but it’s not a good idea to do that only for tests.
To deal with the problem of not having safe default values, we can take the idea of the object mother pattern and use that together with our Lombok-generated builders.
Instead of using the Lombok-generated factory method, we can use the factory method from the object mother. The only thing we have to do is to change the static imports. Basically we are calling Orders.anOrder() instead of Order.anOrder(), for example.
Since we cannot pass around builders as arguments, the code is still a little noisier than our custom builder. Another drawback is that our builder will now always unnecessarily construct defaults for objects whose value we override in our tests.
Describe the target audience All Java developers writing tests in Java based on Test Data Builder.