Great International Developer Summit (GIDS)

Hands-on Unit Testing with JUnit 5/6 - Chandra Guntur

57:04 · 21 Apr 2026 – 24 Apr 2026 · YouTube

About this talk

This talk explores the features and enhancements of JUnit 5 and JUnit 6, particularly in the context of migrating from JUnit 4. The speaker discusses the new lifecycle annotations, such as @BeforeAll and @AfterAll, and highlights the improved handling of test assertions and exceptions. Parameterized tests are introduced as a means to run the same tests with different inputs, which can simplify testing for varying conditions. In addition, the speaker describes the importance of customizing outputs for better readability, utilizing tags for organizing tests, and implementing conditional test execution based on system properties. Throughout the session, the integration of AI in writing tests is addressed, emphasizing the benefits of using agents and skills in conjunction with JUnit testing. Finally, strategies for effective migration using tools like OpenRewrite are detailed, enabling developers to transition smoothly to the newer versions.

Full transcript

Hello everybody. Today we're going to talk about testing with JUnit 5 and JUnit 6. I intentionally did not put any AI in my title. So, sorry. But, we will cover a little bit of AI as we go along, right? Um, I just want to first very quickly cover how many of you use JUnit? Thank you. If you're here, that would make sense. Thank you. Um JUnit 4?

JUnit 5? A few. Okay. JUnit 6? Nobody? Okay, great. Then this talk is for you. What we're going to do is I'm going to very quickly cover a few features that are new in JUnit 5 and 6 compared to JUnit 4. Um, I have the entire source code available on GitHub. There's a QR code link if anyone wants to take a picture. I'll give you a few

minutes to do that now. Um, the link is also present here. You can click the link too. Um, and this presentation is available in its entirety, right? So, you can go set this up and run this on your computer. There is no PowerPoint. Right? There is no deck. This all in my IntelliJ. I'm using IntelliJ to present. There is no other um, deck that I use, okay?

Um, and this all ASCII Doc. So, as long as you set up your ASCII Doc plugin, you should be able to see all this code yourself. Great. Um, as we go along with this, I just wanted you to understand that this entire code that you see um, during the slides is live code, right? I wrote that code myself. AI was not used, of course. But, you know,

you can actually run those code alongside as well and we'll try to do that from time to time if time permits. Right. Very quickly, um, my about I'm going to just defer that because I've already been introduced by Ryan. Thank you so much. Um, so I'm going to skip that part. Very quickly, the project structure, if you are going to try and set this up on your

own, um, looks like so. There's a read me, there's a license, there are a few palm files. I wanted How many of you work with Maven? How many of you work with Gradle? A few of you. Okay, great. Um so I'm just using Maven here, but the equivalent commands will work in Gradle as well. So so we'll cover that if time I have a lot of documentation.

Every All the slides are under docs. Um there are a few images and I have a few classes that we'll cover as we go along, right? I'm going to use an addition I'm going to use a calculator first and I'm going to write some tests for addition, subtraction, um and we'll see how it goes, right? So we'll we'll use those live examples as we go along to

cover what what differences there exist between four, five, and six and how you can get better benefits by coming up to a later version of Java. So back here, how to set up I mean there's a little setup file. I'm not going to talk about it for a for an extended duration, but essentially what we have is a different like a few classes under your main um

and a few classes under your tests that we will use as demos as we move along this code. There are three palm files. There is a palm.xml file that uses JUnit 5, simple JUnit 5. And then there's a palm with a bomb, a bill of materials for JUnit 5. JUnit 5 onwards you now have a bill of materials, which means you just import one single major bill

of materials and you get the versions for all of JUnit dependencies that you need that work together well, right? And then you also have a palm with bomb JUnit 6, which is the JUnit 6 version equivalent of the palm with bomb.xml file. And the way I've got this set up very quickly if I don't if you don't mind me going back to a full screen is under

Maven, I have three projects set up. There's a JUnit 5 project, there's a JUnit 5 with bomb, and there's a JUnit 6 with bomb. It's very easy to set up and the setup instructions are all listed out here if you want to try this out yourself. So in my source code, I have three palm files. I don't know if the screen is visible or not, but you

have three palm files and then set up three Maven projects within the same IntelliJ project, Right? It works. I can disable a few and I can enable the others. Right. So, there is that. And now back to our Let me just close this out. Okay. So, here we are. And let's now switch over to the agenda and see what we cover today. I'm going to spend very

little time on the first five topics, but I just wanted you to be aware of those. So, don't don't worry too much if I'm rushing through those. I'm expecting that these you can find on any kind of internet sites, etc. So, don't worry too much if it is going too fast at a pace. If you do want me to slow down somewhere, you can stop me and

ask me questions. I'm okay with that. All right. So, we'll cover some basics of JUnit 5 and 6. We'll cover how you can customize outputs that appear because now that you're using more AI-driven features to develop, you want to be able to understand the tests that are being generated by it. And when you read some method name that doesn't immediately tell you what the method does, uh

uh customizing your output helps a lot. So, we'll we'll take a look at that. We'll take a look at how you can filter your tests like run a subset of the test based by by using tagging and also by using some sort of ordering if you want a demo of tests, for example. We'll look at how you can improve your testing with parameterized testing. So, repeat the

same test multiple times with different inputs. You can do that. Right? We'll look at some advanced features. Then we look at migration strategies. If you're on JUnit 4, how do you switch to JUnit 5 and 6 easily without without going through a lot of hassles, right? We use OpenRewrite as our example here. Of course, you can also use AI, um for which the last topic AI interaction

does exist. The AI interaction is basically how you set up your agents and skills, and we'll talk and discuss a little bit further about those. So, very quickly, the first five topics I'm going to rush through, but please do feel free to stop me if there are any The testing life cycle. Um JUnit 4, 5, and 6 all provide life cycle annotations. These annotations run before your

test begin or after your test end. So, they are called life cycle annotations. They exist at a class level and they exist at a test level. So, if you have a slew of test cases that you want to run together, you put them into a test class and you can run something that runs before all the test run and something that runs after all the test run.

Right? Similarly, you can have test methods, individual tests in JUnit and you can do something just before the test runs and just after the test runs. This is done so you can have a clean environment that is guaranteed and reproducible for every single test that you run. Okay? Um we'll then take a look at the actual test annotation. This is different from JUnit 4, so we'll cover

how this is different. And we'll also compare these, you know, the differences between JUnit 5 and JUnit 4 counterparts. Now, I only mentioned JUnit 5 and 4 because JUnit 6 and JUnit 5 have very similar structure. So, if you know one, you know the other. JUnit 6 does not make a massive number of changes. Um although it took them 8 years to get the next version of

JUnit from 5 5 to 6. Um the visible feature count is very less. What they've done is change the entire piping underneath. They made it Java 17 ready, they got rid of reflection, they added some CSV parsing improvements, etc. None of which is visible to you. But there's a massive improvement to improve JUnit further going forward. Right? But JUnit 6 took a lot of time and this

time was well invested, although you don't see a lot of differences between 5 and 6. There's some differences, we'll cover that as as we go along. But for most part, JUnit 5 and 6 operate in a very similar way. Awesome. So, let's first take a quick look at annotations. And for those of you who have used JUnit 4, right? Uh I don't know if you're familiar with

the @BeforeClass that runs before a class runs, right? That was JUnit's way of doing things. The newer logic says @BeforeAll. So, before all tests run, you actually run something. This is a static method that prints out before all tests are run. Now, when I tell you this is live code, this code is actually you can click and go to the actual live code here. So, this is

the code where you can see our before all. Is the screen visible to everybody or should I change it to a light screen? This is the This is visible, okay. So, I'm going to just go off of my um distraction-free mode and I'll take a look I'll quickly run this so you get an idea of how this looks like. So, there is a before all and it

prints something called before all initiating addition tests. And then there is an after all that runs after all the tests are run, which is nice. So, I'm first going to run all of this. I'm going to run the test one addition class completely and hopefully you can see that it's first runs a before all call. Run the before all call and run some dummy tests. We'll cover

that later. We can worry about that later on. And then it runs some indented amount of stuff for before each and after each and the actual tests itself. You'll see that this is indented. Let me just minimize this so you can get more of the screen. You'll notice that before all is super indented out um and then you got your indentations for before each and after each

for the dummy test. Nothing happened there. And then you got a before each and it ran some test. After each ran after the test. So, for each test you notice something happened and before every test ran, you notice something else. So, that is the before all that we were talking about. Now, just to make a case in point, if I were to simply take one test and

run this, let us see what the output looked like. Right? I just want to run one single test and you'll notice it ran a before all. It ran a before each and after each and an after all. Right? So, that's the general logic is that it doesn't matter the number of tests you run, the before all will be called once. And if you run more than one

test, the before all is still called once, but the before each is called before each test is run. Right? That's the logic that I want you to carry on with this. Great. Um going back to the actual presentation. Um I'm going to go back to the distraction-free mode so you can see more of the screen. So, that was the after all and actually incidentally we also covered

the before each and the after each methods in that, right? In JUnit 4, these methods were In JUnit 4, these methods were called at before and at after. Confusing terms because you didn't understand what before class and at before meant. It's much more clearer now with JUnit 5, right? And the logic is these are not static methods, these are instance methods, right? That's a big difference you

have to understand. The before all is a static method, the before each is an instance method. That is the one difference that you should be aware of as a takeaway. The test annotation itself is very familiar in JUnit 4, 5, and 6. However, in JUnit 4, it used to come from a different package. The package was org.junit.test. The new package is from the new engine JUnit uses

called Jupiter, and so you got org.junit.jupiter.api.Test as your new test annotation, right? So, you can either go in manually and change this or if you have some recipes with OpenRewrite or if you use some AI agents, they know how to take care of this conversion, right? But, the actual annotation remains the same, at test. Of course, at test itself has some attributes that changed and I want

you to be aware of those changes. There used to be something called an expected attribute. An expected attribute is the kind of exception that gets thrown if you run this test, right? In JUnit 4, you actually had to mention it as an attribute of the test. So, after test, you would have a parenthesis where you would put expected equal to and put the class name of the

exception that you were expecting. Okay? Similarly, you had a timeout where it would you would actually say timeout expected equals and you would actually provide some numeral value there. There are better ways to do it JUnit 5 and JUnit 6, and we'll take a look at that in our code base, right? Now, the annotation is simple, it is just an at test annotation. You get rid of

everything else beyond that. Once again, if you use any kind of converter tools such as open rewrite or if you use both have the capability to understand and take care of this already. It is for you as developers to understand what this looks like, which is why we're explaining that. The next thing I want to share is how you can disable tests. How many of you have

written a test case it doesn't work and then you at ignore it? Come on, for the rest of you, please raise your hands. I know you've done it. I've done it. Right? So, you you have a way of disabling tests and sometimes this is because of a known issue. You know that you wrote the test, it works, but now it's currently failing because you made some small

tweak in business logic and you're too lazy to fix it or let's say there were some circumstances with which you couldn't. But the root cause is we all know we are lazy to fix it. Right? Um so, you can disable a test. But when you disable a test, you can also provide a reason for why you're disabling the test. In this case, I said this is a

dummy test. And now if you don't if you remember there was an output that was printed when we ran our test and showed dummy test. Guess where that is coming from. Right? It actually is printing that this test is disabled because it's a dummy test. You could also write something more meaningful than a dummy test. You could actually write something like this is a known issue. This

is the bug number for this test. Right? So, you are now providing more justification of why you're disabling a test rather than it being ambiguous and you just marking it as at ignore and showing a green flag for everything tested. You have 10,000 test cases, you at ignored 9,999 and you have a green, you know, go production. Right? That's That's not That's not good. And we can

we can avoid that by providing more meaningful text around it. Now, just be There are some gotchas with the at disabled unlike at ignore. Right? The at disabled is not inherited. So, if you have a test case and you have a child test case of that, you can do nested test cases. Right? If you do that, you still have to declare at disabled on the child test

case. It does not get inherited down. So, be aware of that gotcha. Don't assume that just because you disable something at the top, everything else below is a disable. That is not the case. Right? Disabled tests still show in test reports. So, unlike in the past where you had at ignore and you could ignore all 900 9,999 tests, have one test succeed, go to production, now it

is going to show you that you actually did this. You're disabling these tests and they no longer are being evaluated, right? So, you your laziness is now being challenged. So, be aware of that. Awesome. So, a very quick summary of what JUnit looks like is you have an at test. We just saw that its package has changed. Um there used to be there's a before each replacing

an at before, after each, before all and after all that replace and then at disable replaces an at ignore. It's got more constraints where it is more meaningful to us as programmers. Any questions about what we covered as life cycle annotations so far? Good. Awesome. Assertions. How many of you have used assert? Okay. If you've used JUnit, I'm I'm sure you've used some sort of asserts, which

is awesome. Um There is a There is a small difference in the assertions package in in um in JUnit 4, you had assertions being uh delivered through an org.junit.assert. That was the name of the class. With JUnit 5, much more enhanced factory um methods exist in the assertions class. This is under org.junit.jupiter.api.assertions. And be aware that everything that has changed in terms of packaging is not just

because they wanted to move a class around. It has become much more, I guess, Java 17 friendly and so on and so forth. With JUnit 5 and 6, you are absolutely forced to use a more modern version of Java, right? So, they're also improving their own stance on that. So, one of the key differences in assertions between what Java 4 JUnit 4 had and JUnit 5 and

6 have is that the message parameter used to be the first thing that you put when you write your assertion. Now, this is bad because a message may not be required. It's an optional field and you just had to put two double quotes just to get away with not having to write a message, which is silly, right? A message is an aftereffect of what happened, which is

why your test failed. It should be the last constraint, right? So, in the more modern versions of Java JUnit 5, you'll notice that the assert equals first has an expected and actual and then an optional message. Okay? Um not only that, with JUnit 4, you were only allowed to pass in a string. The signature changed um and we just mentioned that the message type is now not

just a string like a hard-coded value, but you can also pass in a supplier. And a supplier allows you to generate different strings based upon what you pass it, which means you've got more flexible messages. And the message can include the fact that there's this output, this this is what the expected output should look like. You can customize your string on the fly. And it is efficient

because it's not concatenated strings, right? So, it makes it much more efficient. Now, there are different kinds of assertions. We'll cover just a basic few for folk who have not heavily used JUnit. Um so, I'm just going to very quickly cover null and boolean assertions first. Um assert null and assert not null. Um you can assert a not null by passing in a value and the message

if needed. You can also skip the message because it's going to give you an actual message saying expected value is null when it should not be null, right? And likewise, for the assert null, you can get the opposite message also, but you can pass your own custom message. Now, this being optional makes a lot of difference because I'm sorry. Um This being optional makes a lot of

difference because you don't have to write a lot of strings, right? You don't have to write a lot of code in order to write your test cases. Similarly, you have an assert true and an assert false. These are boolean logic conditions and you can assert basically a 15D equals addition dot compute first and second where I pass in a 5 and a 10 and the answer should

be a 15. But the same thing when I pass in a 5 and 10 and the answer is 16 should result in a false. So simple JUnit test cases, most of you have probably done these and the code looks like so. You'll notice that there's an assert 15D where the first and second are being passed somewhere. They're probably set in my setup. 5D and 10D, right? So

I'm passing it in 5D and 10D and I'm asserting that they're 15 and 16 not 16, right? So we do that. And once again, everything that you see in code here is actually directly coming in from here. So if I were to go change this to 16 and run this code, this should fail. Let's take a quick look at how the message assertion looks like and I'm

going to run this. Let's run this test. It passed. That is surprising. How did that pass? Okay, I'll I'll come back and figure that out, but um just be aware that when you're running this code on your own on on your machine, be aware that any change that you make in this code is actually going to reflect on the test case also on on the documentation also.

So just be aware that your documentation is changing. So just make a copy of it and do your tests. So I'll change this back to 15 And let's let's go back to our demos. Then you have your equality assertions where assert equals and assert not equals. Um so certain conditions basically here I'm testing two strings to be equal and then I get passed it a message if

I wanted to. There's also an assert array equals where you can test the contents of the array being the same, right? It actually goes through your entire array. It does not actually do a memory location comparison. It looks for the content comparison, which is what the equals does, right? It calls a dot equals method on whatever you could pass it to it. There's also an identity assertion,

which is different from an dot equals assertion. In this case, it's called an assert same and assert not same. And the assert same basically checks for the memory location to be the same for two different objects. So, if an object A points to the exact same memory location as object B, assert same returns a true. And then if they are different objects, for example, in this case,

I actually have a new string, which is a new memory location, then should not be the same, right? So, it is possible for you to have an assert equals return true and then assert same return a false for the exact same values. So, be aware of that. Okay? Know what to use when. Those are the three assertions I wanted to show you because it's a quick overview

of how assertions look like. There are a lot of assertions available to you, right? There's a true false boolean assertion. There's a null not null boolean like null checks. There's equals and not equals, same and not same, identity checks. There's array equals and iterable equals. So, you can do any kind of collection. There's an assert throws, assert throws exactly, and assert does not throw. These are all

with the expectations exceptions. And in the past JUnit 4, expected equal to in the at test is replaced with an actual assertion now. So, you can assert a portion and then do something with the exception, handle it, and move on further in the same test, right? So, it makes it much more fluid. You can also do an assert timeout and assert timeout preemptively. A timeout basically is

how long does your method take in order before it throws a timeout exception? Or you can preemptively say, "After 5 seconds, if the value is not returned, just throw a timeout and then move forward as if there's a timeout happening, right?" So, you can simulate Assert instance of this is a new one. So, in the past, you used to do an assert true and then pass it

an instance of check inside the conditional logic. You don't have to do that anymore. You can simply use an assert instance of. There's an assert all, which is super amazing, where it actually does a join of all your assertion conditions, and if any one of those fails, it returns a false, and then it fails the assertion. So, it's kind of nice that you can actually group assertions

together, have them, you know, spread out as different threads, perform the assertions, and return back one comprehensive Was there a flicker again on the screen? Are you still seeing a flick? Okay. Um I am not moving anything, so I'm just going to hold on and assume that it works. Okay. Bear with me. I don't know what's going on here. All right. So, those were the assertions. Any

questions on what we covered so far on assertions, on lifecycle annotations, anything that stood out, anything not clear? Awesome. How to customize output. So, when you write your tests, you obviously see the method name as as what shows up when you run the test, right? Sometimes it's not very um comprehensive. You don't know what's going on, and you want to actually give it a more meaningful name.

Any of you here use Spock? For those of you who haven't used Spock, Spock originally had this concept where you could put in double quotes a meaningful string of what this test was about. So, JUnit 4 onwards, there have been attempts made in order to make that available in and JUnit as well. JUnit 5 succeeded with display names, and JUnit 6 continues with that, right? There are

two ways of doing display names. You can either set up a display name on a per test basis, where you go into the test and say, "Hey, this is the output I want you to show me when I run this test." Or, you could actually write a generator. Okay. You can also write a generator which simply generates your method names on the fly based upon certain conditions,

and we'll take a quick look at both of those, right? Um so, here is a display name logic. If you notice on the first line over here, does it at display name, and I'm passing it some value, custom display subtraction compute, right? And then I'm calling some methods and lo and behold, instead of it showing compute on your test screen, it's going to show you net display

name. This one I want to run real quick. So, I'm just going to go here and you notice you have a custom display subtraction compute. And it's a test, right? So, if I were to run this test real quick, you will notice on my JUnit screen, it shows Right? It does not show the word compute. Now, I'm going to just going to remove this and show you

how it looks like without. So, if I were to go here run this code now, this is what JUnit Uh, something's not working on my machine. >> I'm done. >> I'm sorry. >> I thought you were done paying it. >> Uh, no, I don't think so. So, let me just do that because I have three Maven projects. Maybe that's what it's doing. Let me just quickly run

the compile. I did get a new version of IntelliJ yesterday. So, I I live dangerously. So, let me just rebuild the project just in case. Thank you for that, though. Okay. So, let's see if this works. Crossing my fingers. Yeah, it works. Okay, thank you. Um so, so it shows compute as a method name before, which was not very meaningful for me to understand. Whereas with a

display name, I can actually do better and understand what it signifies, right? So, I have the ability to customize the test and when I'm looking at it, I know exactly what is going on, what test has succeeded and what failed. So, it sort of helps. So, that's one thing that we could do, uh where I could go into every single test and write some meaningful logic, but

very quickly gets very tedious and you don't want to do that. And when you don't want to do that, there's a better alternate in number one, use AI to do it for you. That's the simplest way in which you're going to avoid doing work. Or, you can actually write what's called a display name generator, right? With a display name generator, what it does is I have at

the class level like in or at the method level. I can put in at display name generation and pass it a display name generator instance. As you can see the calculator display names extends display name generator. Okay, so I'm actually able to produce names on the fly based upon certain constraints and we'll take a look at it in a minute. Um, I can generate a display name

for the entire J unit class and I can also generate a display name for individual methods. I can also do it for nested classes. So, let's go back to the output real quick and you'll notice when I did the run, it printed testing the two subtraction or some which is which is wrong. It's a typo, but you'll notice that it comes out from here. Um, it took

this test two. It took the letter two and the number two and the word subtraction and it created that for me. And how did it do that? It did that because I have a calculator display names and in here you'll notice there's a generate display name for class which parses your entire class name and if it starts with a test, it returns testing the and the class

name. So, I've generated a meaningful name out of what exists on my class name itself. As long as I give it a decent class name, I get a decent J unit Right? I could either do that or I could simply override it and create my own at display name. For instance, I could just do at display name and I could have done this too. So, although I

have a display name generator, I can override it with my own custom display name on top of it. So, these there's some amount of flexibility is provided to you. I'm going to choose to let the display name generation take care of most of my work cuz it's less work for me to do. Right? But it is possible for you to customize as you want. In case if

you notice in the rest of the test don't have any kind of customizations. Okay, great. Going back to the code. So, that's how you can do a generate display name and a generate display name for a method. Um you also The next topic that we want to cover is tagging and ordering of tests. So, in the past, how many of you have used JUnit test suites? >>

[snorts] >> Anybody? Does anyone know the purpose of a test suite? Group test. Oh, sorry. Group test, okay. Anything else we can do with test suites? Order tests. Order the tests. Okay. Um not so much, but but most importantly, the the the answer is in the veins of grouping tests. You can group together a set of tests and say run these. Don't run all my 10,000 tests

for me, run only these 50 tests for now. And let's say you you've made a small change and you want to do a smoke test. Right? You don't need to run all 10,000 tests, maybe you just want to run 50 of those. So, you can group them into a test suite and then pass them along. Now, the problem with the test suite is that code is hard-coded.

Right? You have somewhere in your code hard-coded that these are the tests that you want to do, and that's a compiled Java code. So, you cannot make a change and add a 51st test on the fly. Right? And that causes problems. However, there are some ways in which you can improve upon that by what is known as tagging. Each test I'm so sorry about the flicker. I

don't know is it readable? Can everybody still read? you can now add tags on your test cases. So, here for example, I have a tag called passing test. So, this test actually passes, and I also have another tag called it's an addition test. So, I can create some sort of logic where I run all tests that have the tag passing in them. Or run all tests that

have the tag addition in them. Or I could even write run all tests that have the tag passing but not the tag addition on them. Right? So, there are different combinations you can do and this is much more flexible than writing a test suite. Right? That being said, JUnit 5 and 6 also have some improvements on the test suite. We will cover that. But I just wanted

you to know this is a much more flexible way of you configuring how to write these tests. I'm going to show you one example where this looks um easier to understand. So, I have this code here. Let's go to the palm XML file real quick so I can show you where I can customize what should run and what shouldn't. Right? So, I get rid of my at

disabled and I can actually, if you notice in my configuration for the Surefire plugin, I can pass it groups of passing and I can exclude all failing Okay? So, that's much better than me having to write an at ignore everywhere. I know certain tests fail, I can mark them as failing tag and I can ignore that tag completely. Right? So, it makes my output much more easier

to understand and comprehend. Reduces the number of gibberish lines I have to go through in order to understand what's happening with my code. Right? So, this is one way. You can do the same thing with Maven where you could actually pass it a minus D groups equals and pass whatever comma separated tags and would only pick those tags up or exclude those groups if you wanted to.

Right? So, for instance, uh I don't know if I still have it in my buffer, but there you go. So, Maven clean test minus D groups equals passing. You can pass something like that. Right? So, you can actually limit what you're doing and that is very auditable in your build command. Right? So, if you are in an audit required industry and you do that, at least your

build command is very auditable. Right? So, you have not done an at ignore, at disable, or whatever and you clearly made your intentions um auditable. So, it's kind of beneficial. All right, going back to the distraction-free mode. Okay. Um this is one great thing to do. You can tag. Tags are much better than uh you having to do some sort of magic in order to create test

suites. We'll go through test suites in in a in a moment. I just wanted you to know that tags are a much better alternate. The other thing that you can do with JUnit, and I implore, beg you not to ever do this, is you can order tests, test ordering is a code smell. If you're ordering tests, that means that you depend on the outcome of some previous

test. That is the only reason I can think of where you order tests. So, if you're ordering tests, there must be something wrong, or you're presenting in a presentation or a demo, right? Those are the only two logical reasons of why you need an ordering in methods. So, if you're not presenting a demo as a speaker somewhere, you're doing something wrong by ordering it. That's all my

my justification is. However, if you still want to do it, um, JUnit allows you to do what's called a test method order. Here I use a method orderer order annotation. So, it says that I'm passing it an ordering mechanism through the order annotation. As you notice, my test method will have an @Order annotation where I pass it number one. Then, the next one will be number two,

and so on and so forth. It'll execute the tests in that specific order. Now, can someone tell me what happens if I don't order my tests? Anyone? Okay, no one answer is go random. Anyone else any answer? I have five tests. T1, T2, T3, T4, and T5. I don't put any ordering. Can someone tell me what order they'll be executed in? >> Based on alphabetical order of

the name. >> Alphabetical order of display name. Any other takers? Okay, your answer was the closest. obviously. It is random. You cannot determine what order JUnit will run its tests in. Especially after Java 25, there is absolutely no way for you to know which order it will run it, right? So, always be aware that your test should be very isolated and siloed and not depend on anything

else outside the scope of the test to run, right? If you are doing that, you're not writing a unit test anymore. So, keep Make sure that your unit tests are extremely localized to testing exactly one thing. And why are we talking about this in the age of AI, right? Um Most of us would say AI is writing our tests. I don't have to worry. You still have

your name associated with that commit. Right? You have to review the code that the AI wrote, and you have to make sure that it is doing the right things. So, you still own the responsibility, although you didn't write the code. So, make sure you see what it is doing, and make sure that the outputs are matching with all the expectation that you have if you had written

the test yourself, right? That's important to know. Now, the coolest features that JUnit 5 and 6 introduced are parameterized tests. You can parameterize a test I mean, they also existed in JUnit 4, but they were a bit more confusing. With JUnit 5, you can have different kinds of sources that allow you to repeat the same test method with different input values. So, we'll take a quick look

at value sources first. And this is a bit lengthy of a slide, so bear with me. I'm going to walk through a few use cases The first source you will have First value source you have is a null source, which means I'm testing for nulls. Now, when you write your AI code and when AI generates your tests, the first test it skips, I don't know if you've

noticed this, is null check tests. You actually have to specify to tell it, "Hey, please add a null test for me." Right? And that is a skill that AI does not naturally have, and you have to encourage it to do that. And you can do that by telling it to use parameterized tests. And we'll take a look at it in our instructions for AI, but a null

source is a very important way of first validating an assertion of producing null. Right? If your input is null, what happens? In this case, with a null source, when I pass an N integer, its value is going to be null, and it will test that this this integer is null. Right? This is a simple test case in my case, but you could have a more complex business

logic associated with nulls. It's very important to have a null source. You can also have what's called a value source. A value source is basically a string array in my case. You could have any kind of array. You could have object arrays. Um you could have objects declared elsewhere. You could have a list declared elsewhere and then pass it in, or you could declare it inside your

value source. Here I have strings equals a few strings I have, and then those strings feed the attribute person name, and once I have the person name, I can run the test for each person name. And when I run this, it actually prints multiple lines. One for Alice, one for Bob, one for Chan, et cetera. So, let's take a quick look at it after I'm done with

the value sources. The third thing you can do with a parameterized test is, remember how we had customized display names earlier, and we were able to generate different custom names? Your parameterized test also can customize their names by passing it an index zero is greater than two cars. Index being the index number of 0 1 2 3. Zero is the zeroth value, which is the zeroth attribute,

person name. So, if this would print zero {comma} Alice is greater than two characters, one {comma} Bob is greater than two characters, et cetera. Right? So, it does that kind of customization of display names for you. And then you can also use what's called an enum source. So, you have an enum already declared somewhere, and you can run it against all values of an enum. This is

a very useful business use case, because if you have business logic where you different define different states. Right? For example, you're writing a state machine somewhere, and you have You created an enum out of it. Now, you want to do test the same method against different states. How is it reacting? This is your test case right there. So, you have an enum source. In my case, I

use day of the week the Java provides. And then I'm verifying that that every day of the week ends with the letter D and ends with the letter D A Y. Simple test, but I'm trying to make a point here, right? So, each day of the week name ends with day and it's going to run this and it's going to print exactly this. So, let's quickly take

a look at this code. Run it real quick and then make sure this works just like I told you and I'm not making something up. So, parameter based test, you notice I did a check now. Oh, obviously I don't have my test ordered. We I was telling you not to order test and I'm a practitioner of what I'm telling you. Uh verify person names None of these

have display names. However, you notice that the moment I created these test cases Alice, Bob, Chan, David, Evgeny, and Fabian, it printed them out as 1 2 3 4 5 6. These are customized parameterized test names that are repeating themselves. There are other parameter tests that we ran. Let's take a look at that. So, Monday, Tuesday, Wednesday, etc. Each one of these is actually being repeated multiple

times, right? So, you are repeating the test and let's take a quick look at the test themselves. The test is this simple and where I'm verifying this and it ran as many times as I asked it to run. Now, the verify person name is longer than two characters still shows. You'll notice it still shows as that test because I've not added a display name there. But if

I add a display name on top of it, imagine how customized my JUnit 5 output looks like, right? So, it's going to make your output much more simpler and you can actually ask AI to generate these for you. So, so be aware of what is possible. It will not do it for you, but you have to know that it's possible to let AI know to do it.

So, that's where these come in handy. Knowing these comes in handy. These are great. Right? These are value sources, enum sources, null sources, etc. are great. What about CSV sources? Right? And this is a bane of problems in JUnit 6 that you should be aware of. JUnit 5 had a very lenient CSV parser. You could get away with a lot of wrong stuff. It would forgive you

and it would treat it as CSV. JUnit 6 no longer is that forgiving. So, a lot of your test cases that worked with JUnit 5 will absolutely fail with JUnit 6 if you're using CSV logic. Okay? Your CSV has to be much more thorough and correct. Right? And it's intentional because you did not know if it was a mistake in the CSV or if you intentionally created

something that you wanted to fail. because of that there there was a decision made to make sure that the CSV parsing with change in JUnit 6 is much more non-lenient or thorough or strict. Right? So, here are a few examples. And there's a CSV source I could use which is nothing but a comma separated values list. In this case, I have some gibberish string and its uppercase

equivalent. And all I'm testing is that to uppercase works. Right? So, when I do a to uppercase on this first parameter, it should result in the the expected value should match what the actual is. The actual being the first parameter.toUppercase. Here you have the expected. Here you have the actual which is um my random case name name to uppercase which is the actual value and they should

be the same. Right? And I'm repeating that 1 2 3 4 5 6 times. Right? So, six different times the same method is going to be called and executed because I'm passing it a CSV source. And we already saw the output for this. If you notice in our output, somewhere in here we have What was the name of this method? verify uppercasing names. They should be verify

upper casing names here. So, this method. So, you notice that this is this run six times, right? So, that's how this would work. Um you can also pass in a CSV file source. So, instead of you having all the CSV listed inline in your Java code, you could actually have a CSV file that's under your test resources, and it picks that up and it it parses it,

processes the same, right? It's the exact same code I have in the upper case names CSV file. I can very quickly show that file to you. If you notice here, under my test resources, I have upper names.csv. That's exactly how it looks. So, it's either able to pick up from a file or with as from within the inline Java sources. Both are possible. Great. How are we

doing on time? Okay, I have 15 minutes left, so I'm going to try and rush this up. So, the advanced features that you can avail of in JUnit 5 and 6 are you can do customizations based on if you're running your test in different operating systems, you can do different tests. So, if you're running on Mac OS, you can run the test. If you're running on Windows,

you can run an alternate test. You could enable or disable JREs. So, if you're running on JRE 11, you don't run the test. If you're running on JRE 17, you can run the test. You can also mix and match. You can say, "If I'm running on eight and 25, run this test. If I'm running on 11 and 17, don't run this test." So, you can do JRE

ranges also, right? There are different things possible in with the JUnit 5 and 6, and there's like a ton of stuff here. I don't want to go through everything, but know that you can actually do disable and enable on different things, on different kinds of I guess operating systems, system properties, environment variables, JREs, JRE version ranges, etc. All with JUnit 5 and 6. It's pretty amazing, right?

Here are some examples. Here, for example, I'm enabling this test on Java 17. I'm disabling this particular operating systems from executing this test. So, if I'm on a Linux or a free BSD, this test will not Well, I can either enable or disable, right? Pretty cool. So, a full summary of what is possible, what conditional logic I can use is enable on operating system, GRE, GRE range,

system property, environment variable, and if condition. You can actually set an if condition say, "If this condition is met, run this test." So, that's super cool. Um enable the native images. So, you can actually create native images like containers, etc. And you can run some tests for that. If you're not on a native image, you can disable the test, etc. And then you have the at disable

command to ignore everything and just not run your test, right? So, there are different kinds of annotations which help you deterministically run tests that only run on specific environments. And this is because, let's say there are Indian differences between uh a Mac OS and a Windows. Or there is some other difference in file paths that you want to test across and let us say a Windows machine

versus a um a Mac OS, right? You could do write different tests and enable or disable based upon the operating system, right? It's kind of cool features. I like it. The next topic, which is a lot of people get confused about and probably don't use because they're confused about is called assumptions. So, you have assertions and then JUnit also provides what's called assumptions. So, assumptions are something

very interesting in that they are they are a way to check external constraints. Not constraints that you have in your JUnit file, but outside of that to run or not run a test, right? In this case, I am assuming true that something exists outside of my JUnit test case. And if it does, I run the test underneath it. If it doesn't, then I don't run the test.

So, in this case, my assumption is there is a system property that I have set up elsewhere, okay? Uh for fake demo purposes, I set this up over here as set property, but most likely your system property set elsewhere, right? And then my assumption on the JUnit I'm saying, if the value is yes for that particular system property, run the code. If it is not, then abort

the So, the test gets aborted and it actually throws a test aborted exception if your assumption fails. This we can show you real quick in code where here I have a run if assumption met. I actually have this code with something will print before the any assumptions are run. And then I'm going to do an assume true, and if the value is true, not no but a

yes, then it would have run the test further. Like the test below it would run. This line would have printed if this was a yes. However, if the assumption is not met, this is like a ternary logic, either this or this, right? So, in that case it would actually go to the test aborted method and run that code, which is nothing but it says this test was

aborted. Okay? So, you have the capabilities of either running um based on assumption being true or not. That's a very important um way to control your test cases. Knowing that if an assumption is met, maybe a slew of test cases don't need to be run. Right? And you can actually control saying, "Hey, this is only for a smoke test." Or "Hey, this is only for a pre-prod

or a parallel prod parallel run. Don't run these tests." You can control that with a system property. You can also control it's an environment variable, right? You can use environment variables equally um instead of a system I believe I have examples of environment variables also out here, but the next test that I wanted to show you that is super cool is the other assumptions. You got to

assume true that we just saw as a demo. There's an assume false, which is the exact opposite of assume true. But the most important one is assuming that. That's very, very powerful, right? Um so, assume true and assume false stop the rest of the method from executing and assuming that only protects the block of code that you're trying to execute and either runs that or doesn't run

it. So, let's take a quick look. Going back into the code. So, I have my assuming that logic here. Hopefully, everyone can read the screen, Okay. So, I have my system property set to no and assuming that yes is the value of the system property, I actually have a runnable and this the the run will not work because it's not the same. Then, I'm showing you that

the next line will actually break. It's not aborting the rest of the method. So, only this portion is not evaluated, right? And the rest of the code below will still be executed. So, I'm executing this will also print. And then I set the property to yes to demo that it actually works. Excuse me. And when I run that again, it actually says that my assumption is met

and it'll run this code for you. So, you can actually control portions of your JUnit from working or not working based upon your assumptions. So, either you can use an assume true or an assume false, which are much more simpler and they will not print anything below or work on anything below the assumption if the assumption is not met. However, you have something called assuming that, which

only protects a portion of your test and if that doesn't work, the rest of the test will still continue to execute. So, you have more control and flexibility on assumptions. Okay? So, you can assume stuff. Anyone have any questions about assumptions? >> You can put the condition >> Right. So, how is it different from the conditions that you showed in the earlier slide, the enabled and the

disabled? The logic is with enabled and disabled, you have a known set of constraints. These are known values, such as the JRE version number, your operating system, or your um system property, or your environment variable, right? Those are all very well-known, well-identified values that you can look at. You can have some functional logic. >> No, I mean by piggybacking show the side lyric, if conditions also put

it to enable a >> Yes, enable this, disable this. >> I think you have key what >> Correct. So, the question The question is, how is it different How is it different from using assumptions versus using an at enabled if, for example? And the answer is an at enabled if is still based on a uh an expression rather than a function call. It is more static than

this being dynamic. So, this can actually reach out to a database, perform some activity, and come back and tell you whether or not to run a test. Is it a good idea? No, but is it possible? Yes. Right? So, assumptions give you more reach than what an enabled if and disabled if do. But are they continuity of that? Yes, the answer is yes. Did I answer your

question? Awesome. So, that was the features that I wanted to cover. I don't want to spend too much time on features, but I did, and I only I'm left with 10 minutes, 5 minutes. Right? So, migrating. This is very important. Um you're probably on JUnit 4, and you want to migrate to JUnit 5 or 6. Um use Open Rewrite. Right? That's my way of doing it. Maybe

use some other tool. I'm just going to show you how Open Rewrite works. Right? Um it has a recipe, JUnit 4 to 5 migration. There's also a JUnit 4 to 5 test case migration you could look at. There are a few recipes uh available online if you go to Open Rewrite recipes. But the one thing One thing that I've noticed is when you run these recipes, you

also want to run the JUnit 5 best practices afterwards. So, it actually cleans up and and parameterizes a few tests. This one does not do test test case parameterization for you. So, this simply does a drop-in replacement, changes the assertions, changes the assumptions, etc. You also run this on top of that. The other thing that you would do for JUnit 5 6 migration if you have that

is you want to basically run this particular recipe. JUnit 5 to 6 migration is pretty cool. I've actually done this on my code and it works and it showcases some problems with CS for me. The commands are pretty simple to run these. There's a Maven command and the Gradle command. I don't think I want to spend too much time on this. I'll just share this code with

you anyways. The key changes are essentially, you the before each after each and the before all after all in the newer versions. It removes all kinds of unnecessary public modifiers. The assert moved to assertions, assumes moved to assumptions, and then your error error message argument moves to the end of the method, and then the test expected and test timeouts have been replaced with assert throws and assert

timeout. Right? Run with that something a lot of Spring Boot uses. The run with command is now known as extend with. They're now called extensions. There is an example of extensions in this code where I have a random name generator. I can show that to you anyone who's interested And then there are like superficial changes that JUnit 4 and 5 brought about with temporary folder versus temp

dir. Right? What I do want to focus on the remaining 5 minutes on is AI, right? A lot of us have been, you know, focusing on AI or have been have been dumped with focusing on AI by where we work. There are two kinds of models that which you should be aware of in with testing. You can either use agents which are supposed to be global and

always available to your AI agent or your skills which are very specifically targeted for certain prompts that you provided. So, make sure you know the difference between the two. Your agents you should always leave open and you can always leave open-ended, but your skills need to be extremely specific and to the point. So, just make sure your language is very very clear on both your agents and

your skills, right? An agent is at a global level Um, then a skill is usually at a local task specific level. Right? Um, an agent is always loaded and active. So, as soon as you start your chat client or your AI client or whatever you call it, um, it is active. The skill is loaded on demand. So, you actually tell it when to start loading the skill

up. Right? Um, the purpose of an agent is basically to provide a project wide context. It's more generic. Um, so you want to leave generic context in your agents. Like, here for example, I say always use J Unit 6 in Java 25. That's generic. There's nothing specific about it. With skills, you want to be much more, you know, specific to the point where you say generate parameterized

test for this class, etc. Like, so so make it much more instructional. And in skills, you actually tell what role that particular agent is playing for you. So, very quickly on the agent side, for example, I have an agents.md file here. Um, what you would put in there is, um, use Java 25 and Maven. Use J Unit 6. Do adhere to CSV because I know J J

Unit 6, um, has more strict CSV parsing. You're prohibited. You can add prohibitions in your agents files, right? Um, you want to mention that you want to use display name for all your classes that get generated. So, it's easier. Follow TDD principles. Now, there's a lot of debate on this. I'm not a I'm not a 100% proponent on TDD. I'm with AI, I'm a more 100% proponent

on BDD. And I do J Unit test following that because a behavior is what you're trying to capture with AI. You give it a prompt saying I want to do this. You don't You don't give it a class name and say I want it to build this class. You say I want to do this logic. Right? So, it's behavior. So, you want to write your behavior development

driven development test first. And then from there, you actually generate J Unit test cases. One thing I do recommend is always put in a positive into negative scenarios for whatever you're writing. And use parameterized tests. So, these are some useful hints and suggestions you can provide to for agents. Skills, obviously, you want to do one as a an expert and one as a regular JUnit user, right?

In the skills, I I actually put some patterns like use parameterized test at parameterized test. Um stricter CSV parsing is required. And then I could do an assert for J testing can be used. I can say Mockito extend with, right? And then I also put prohibitions and constraints saying no no JUnit features, avoid at order, keep the context clean, etc., right? You can also do something that

is not an expert where you're just running run-of-the-mill tests. And here I have some core structures. Um make sure that all our tests have similarly named. Follow the triple A pattern. I mean, if you use triple A pattern for your JUnit test cases, if you're not, it's basically arrange, act, and assert. So, you have dif- different sections, given, when, then kind of sections. So, you want your

test cases that are generated to look the same, right? Uh each test should have a clean slate to start with, so have a before each. Uh with JUnit 6 in Java 25, you get virtual thread support. So, you can actually set the mode as concurrent and start using virtual threads. Um you just wanted to be aware of CSV parsing, and you want the guardrails, right? You don't

you want deterministic results, you want your JUnit test cases to exactly solve one problem and validate it. You don't want it to be non-deterministic. You want names to be descriptive, you want display names to be generated for everything. So, the presentation portion is done. If anyone has any questions, I'm happy to answer. Um I can also show some code um related to both suites as well as

related to extension tests, if anyone is interested. The gotcha that we talked about with the CSV, right? If I were to run this code real quick, um this code works fine with JUnit 5. It actually succeeds because it read it as foo and bar. Now, if I were to run the same code with JUnit 6, it's going to fail because it would say that my delimiter string

is the same as a comment character. Let me see if I have time to do that. Um I'm going to ignore this project. And I'm going to ignore this project. By the way, this is how you run the different Maven POMs within one single IntelliJ, and that's what I'm doing here. And if I now were to run this clean and test, right? Hopefully, it'll show an error.

There you go. It failed with an error, and if you look at the error, the message is very clear. It tells you that your test JUnit 5 versus 6 the pre-condition violated the delimiter string, the quote character, and the comment character must all differ. How can I solve this? You can make it a bit more um non-lenient. You can make it a bit more strict. I don't

want to use the word non-lenient. And you can do this. Right? If I were to run this code uh with JUnit 6, it would work. I still have to compile my code. It's not compiling, so I'll have to run a build. I don't have the time to do that, but if anyone wants to see the demo for this right after this session, I can show that to

you. Awesome. Thank you so much for your time. If you have any questions, I'll be outside. >> [music]