mockito: Mockito 5 lab (inline mock maker, static/construction mocking, captors, BDD, strict stubs)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cfo2K4u4rF18QsPXPFnytf
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package lab.mockito;
|
||||
|
||||
public interface AuditLog {
|
||||
void record(String event, String detail);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package lab.mockito;
|
||||
|
||||
public class CheckoutService {
|
||||
private final PaymentGateway gateway;
|
||||
private final AuditLog audit;
|
||||
private final PriceCalculator calculator;
|
||||
|
||||
public CheckoutService(PaymentGateway gateway, AuditLog audit, PriceCalculator calculator) {
|
||||
this.gateway = gateway;
|
||||
this.audit = audit;
|
||||
this.calculator = calculator;
|
||||
}
|
||||
|
||||
public Receipt checkout(String account, long cents, int discountPercent) {
|
||||
if (!gateway.isAvailable()) {
|
||||
audit.record("CHECKOUT_REJECTED", account);
|
||||
throw new IllegalStateException("gateway unavailable");
|
||||
}
|
||||
long payable = calculator.applyDiscount(cents, discountPercent);
|
||||
Receipt receipt = gateway.charge(account, payable);
|
||||
audit.record("CHECKOUT", account + ":" + payable);
|
||||
return receipt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package lab.mockito;
|
||||
|
||||
/** Pretend this talks to an SMTP server. Real sending is not what the tests are about. */
|
||||
public class EmailClient {
|
||||
public EmailClient() {
|
||||
throw new IllegalStateException("real SMTP connection attempted");
|
||||
}
|
||||
|
||||
public EmailClient(String host) {
|
||||
throw new IllegalStateException("real SMTP connection attempted to " + host);
|
||||
}
|
||||
|
||||
public boolean send(String to, String body) {
|
||||
throw new IllegalStateException("real email sent to " + to);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package lab.mockito;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Static helpers: hard to substitute without mockStatic. */
|
||||
public final class Ids {
|
||||
private Ids() {}
|
||||
|
||||
public static String newOrderId() {
|
||||
return "ORD-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
|
||||
public static long nowMillis() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package lab.mockito;
|
||||
|
||||
/** Builds its own EmailClient with `new` - no injection point, so only mockConstruction can intercept it. */
|
||||
public class NotificationService {
|
||||
public boolean welcome(String user) {
|
||||
EmailClient client = new EmailClient("smtp.example.com");
|
||||
return client.send(user + "@example.com", "Welcome, " + user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package lab.mockito;
|
||||
|
||||
public class OrderService {
|
||||
public String place(String customer) {
|
||||
String id = Ids.newOrderId();
|
||||
return id + "@" + customer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package lab.mockito;
|
||||
|
||||
public interface PaymentGateway {
|
||||
Receipt charge(String account, long cents);
|
||||
|
||||
boolean isAvailable();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package lab.mockito;
|
||||
|
||||
/** A final class with a final method: the classic "Mockito 1/2 could not mock this" case. */
|
||||
public final class PriceCalculator {
|
||||
public final long applyDiscount(long cents, int percent) {
|
||||
return cents - (cents * percent / 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package lab.mockito;
|
||||
|
||||
public record Receipt(String id, String account, long cents, boolean approved) {}
|
||||
@@ -0,0 +1,62 @@
|
||||
package lab.mockito;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BddStyleTest {
|
||||
|
||||
@Mock PaymentGateway gateway;
|
||||
@Mock AuditLog audit;
|
||||
@Mock PriceCalculator calculator;
|
||||
|
||||
@Test
|
||||
void givenWhenThen() {
|
||||
Transcript t = new Transcript("08-bdd-style.txt");
|
||||
// given
|
||||
given(gateway.isAvailable()).willReturn(true);
|
||||
given(calculator.applyDiscount(1000, 10)).willReturn(900L);
|
||||
given(gateway.charge("acc-1", 900)).willReturn(new Receipt("r-1", "acc-1", 900, true));
|
||||
CheckoutService svc = new CheckoutService(gateway, audit, calculator);
|
||||
|
||||
// when
|
||||
Receipt receipt = svc.checkout("acc-1", 1000, 10);
|
||||
|
||||
// then
|
||||
assertTrue(receipt.approved());
|
||||
then(gateway).should().charge("acc-1", 900);
|
||||
then(audit).should().record("CHECKOUT", "acc-1:900");
|
||||
then(audit).shouldHaveNoMoreInteractions();
|
||||
InOrder order = Mockito.inOrder(gateway, audit);
|
||||
order.verify(gateway).charge("acc-1", 900);
|
||||
order.verify(audit).record("CHECKOUT", "acc-1:900");
|
||||
t.line("receipt approved : %s", receipt.approved());
|
||||
t.line("then(gateway).should() : charge(\"acc-1\", 900) ok");
|
||||
t.line("then(audit).should() : record(\"CHECKOUT\", \"acc-1:900\") ok");
|
||||
t.line("shouldHaveNoMoreInteractions on audit : ok");
|
||||
t.line("InOrder gateway.charge -> audit.record : ok");
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void willThrowAndNeverCalled() {
|
||||
Transcript t = new Transcript("09-bdd-throw-and-never.txt");
|
||||
given(gateway.isAvailable()).willReturn(false);
|
||||
CheckoutService svc = new CheckoutService(gateway, audit, calculator);
|
||||
|
||||
IllegalStateException e = assertThrows(IllegalStateException.class, () -> svc.checkout("acc-2", 1000, 10));
|
||||
then(gateway).should(never()).charge(anyString(), anyLong());
|
||||
then(audit).should().record("CHECKOUT_REJECTED", "acc-2");
|
||||
t.line("exception : %s", e.getMessage());
|
||||
t.line("gateway.charge never called : ok");
|
||||
t.line("audit recorded CHECKOUT_REJECTED : ok");
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package lab.mockito;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CaptorTest {
|
||||
|
||||
@Mock PaymentGateway gateway;
|
||||
@Mock AuditLog audit;
|
||||
@Captor ArgumentCaptor<String> detail;
|
||||
@Captor ArgumentCaptor<Long> cents;
|
||||
|
||||
@Test
|
||||
void captureWhatTheCodeActuallySent() {
|
||||
Transcript t = new Transcript("06-argument-captor.txt");
|
||||
when(gateway.isAvailable()).thenReturn(true);
|
||||
when(gateway.charge(anyString(), anyLong())).thenReturn(new Receipt("r-1", "acc-9", 900, true));
|
||||
CheckoutService svc = new CheckoutService(gateway, audit, new PriceCalculator());
|
||||
|
||||
svc.checkout("acc-9", 1000, 10);
|
||||
svc.checkout("acc-7", 500, 0);
|
||||
|
||||
verify(gateway, times(2)).charge(anyString(), cents.capture());
|
||||
t.line("charged amounts : %s", cents.getAllValues());
|
||||
assertEquals(List.of(900L, 500L), cents.getAllValues());
|
||||
t.line("last captured : %d", cents.getValue());
|
||||
|
||||
verify(audit, times(2)).record(eq("CHECKOUT"), detail.capture());
|
||||
t.line("audit details : %s", detail.getAllValues());
|
||||
assertEquals(List.of("acc-9:900", "acc-7:500"), detail.getAllValues());
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getValueBeforeAnyCaptureFails() {
|
||||
Transcript t = new Transcript("07-captor-empty.txt");
|
||||
ArgumentCaptor<String> empty = ArgumentCaptor.forClass(String.class);
|
||||
Exception e = assertThrows(Exception.class, empty::getValue);
|
||||
t.line("%s: %s", e.getClass().getName(), e.getMessage());
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package lab.mockito;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedConstruction;
|
||||
|
||||
class ConstructionMockingTest {
|
||||
|
||||
@Test
|
||||
void mockConstructionInterceptsNewInsideTheClassUnderTest() {
|
||||
Transcript t = new Transcript("05-mock-construction.txt");
|
||||
// Without mockConstruction the real constructor throws - that is the point of the demo.
|
||||
IllegalStateException real = assertThrows(IllegalStateException.class, () -> new NotificationService().welcome("bob"));
|
||||
t.line("no interception : %s", real.getMessage());
|
||||
|
||||
try (MockedConstruction<EmailClient> constructed = mockConstruction(EmailClient.class,
|
||||
(mock, context) -> {
|
||||
t.line("constructor args : %s", context.arguments());
|
||||
t.line("construction # : %d", context.getCount());
|
||||
when(mock.send(anyString(), anyString())).thenReturn(true);
|
||||
})) {
|
||||
boolean sent = new NotificationService().welcome("bob");
|
||||
t.line("welcome(\"bob\") : %s", sent);
|
||||
t.line("instances built : %d", constructed.constructed().size());
|
||||
assertTrue(sent);
|
||||
assertEquals(1, constructed.constructed().size());
|
||||
verify(constructed.constructed().get(0)).send("[email protected]", "Welcome, bob");
|
||||
t.line("verified : send(\"[email protected]\", \"Welcome, bob\")");
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package lab.mockito;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.plugins.MockMaker;
|
||||
|
||||
class MockMakerTest {
|
||||
|
||||
@Test
|
||||
void inlineMockMakerIsTheDefaultAndMocksFinalTypes() {
|
||||
Transcript t = new Transcript("01-mock-maker.txt");
|
||||
MockMaker maker = Mockito.framework().getPlugins().getDefaultPlugin(MockMaker.class);
|
||||
t.line("default MockMaker : %s", maker.getClass().getName());
|
||||
assertTrue(maker.getClass().getSimpleName().contains("Inline"));
|
||||
|
||||
PriceCalculator calc = mock(PriceCalculator.class);
|
||||
t.line("PriceCalculator is final : %s", java.lang.reflect.Modifier.isFinal(PriceCalculator.class.getModifiers()));
|
||||
t.line("mock class name : %s", calc.getClass().getName());
|
||||
t.line("mock class == real class : %s", calc.getClass() == PriceCalculator.class);
|
||||
|
||||
when(calc.applyDiscount(1000, 10)).thenReturn(42L);
|
||||
t.line("stubbed final method : applyDiscount(1000, 10) = %d", calc.applyDiscount(1000, 10));
|
||||
t.line("unstubbed final method : applyDiscount(1000, 20) = %d", calc.applyDiscount(1000, 20));
|
||||
assertEquals(42L, calc.applyDiscount(1000, 10));
|
||||
assertEquals(0L, calc.applyDiscount(1000, 20));
|
||||
|
||||
Receipt r = mock(Receipt.class);
|
||||
t.line("mocked a record : %s", r.getClass() == Receipt.class);
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package lab.mockito;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.exceptions.base.MockitoException;
|
||||
|
||||
class StaticMockingTest {
|
||||
|
||||
@Test
|
||||
void mockStaticReplacesTheMethodOnlyInsideTheTryBlock() {
|
||||
Transcript t = new Transcript("02-mock-static.txt");
|
||||
String before = Ids.newOrderId();
|
||||
t.line("before scope : id starts with ORD- : %s", before.startsWith("ORD-"));
|
||||
|
||||
try (MockedStatic<Ids> ids = mockStatic(Ids.class)) {
|
||||
ids.when(Ids::newOrderId).thenReturn("ORD-FIXED");
|
||||
String placed = new OrderService().place("alice");
|
||||
t.line("inside scope : place(\"alice\") = %s", placed);
|
||||
assertEquals("ORD-FIXED@alice", placed);
|
||||
|
||||
ids.verify(Ids::newOrderId);
|
||||
ids.verify(Ids::newOrderId, times(1));
|
||||
t.line("inside scope : unstubbed Ids.nowMillis() = %d (default answer, not the clock)", Ids.nowMillis());
|
||||
assertEquals(0L, Ids.nowMillis());
|
||||
}
|
||||
|
||||
String after = Ids.newOrderId();
|
||||
t.line("after scope : id starts with ORD- and is not FIXED : %s", after.startsWith("ORD-") && !after.equals("ORD-FIXED"));
|
||||
assertNotEquals("ORD-FIXED", after);
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void staticMockIsThreadLocal() throws Exception {
|
||||
Transcript t = new Transcript("03-static-thread-local.txt");
|
||||
AtomicReference<String> seenByOtherThread = new AtomicReference<>();
|
||||
try (MockedStatic<Ids> ids = mockStatic(Ids.class)) {
|
||||
ids.when(Ids::newOrderId).thenReturn("ORD-FIXED");
|
||||
Thread other = new Thread(() -> seenByOtherThread.set(Ids.newOrderId()));
|
||||
other.start();
|
||||
other.join();
|
||||
t.line("test thread sees : %s", Ids.newOrderId());
|
||||
t.line("other thread sees : real id, not FIXED = %s", !"ORD-FIXED".equals(seenByOtherThread.get()));
|
||||
}
|
||||
assertNotEquals("ORD-FIXED", seenByOtherThread.get());
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void registeringTheSameStaticTwiceOnOneThreadFails() {
|
||||
Transcript t = new Transcript("04-static-already-registered.txt");
|
||||
try (MockedStatic<Ids> first = mockStatic(Ids.class)) {
|
||||
MockitoException e = assertThrows(MockitoException.class, () -> mockStatic(Ids.class));
|
||||
t.block("MockitoException:", e.getMessage());
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsRealMethodsKeepsTheRestOfTheClassReal() {
|
||||
Transcript t = new Transcript("04b-static-calls-real-methods.txt");
|
||||
try (MockedStatic<Ids> ids = mockStatic(Ids.class, CALLS_REAL_METHODS)) {
|
||||
ids.when(Ids::newOrderId).thenReturn("ORD-FIXED");
|
||||
long now = Ids.nowMillis();
|
||||
t.line("newOrderId() = %s", Ids.newOrderId());
|
||||
t.line("nowMillis() is the real clock (> 0) : %s", now > 0);
|
||||
assertTrue(now > 0);
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void coreJdkClassesCannotBeStaticallyMocked() {
|
||||
Transcript t = new Transcript("04c-static-mock-system.txt");
|
||||
MockitoException e = assertThrows(MockitoException.class, () -> mockStatic(System.class));
|
||||
t.block("MockitoException:", e.getMessage());
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package lab.mockito;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.exceptions.misusing.PotentialStubbingProblem;
|
||||
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
/** Uses the MockitoSession API so the strictness failures can be caught and recorded. */
|
||||
class StrictnessTest {
|
||||
|
||||
private static void finish(MockitoSession session) {
|
||||
try {
|
||||
session.finishMocking();
|
||||
} catch (UnnecessaryStubbingException ignored) {
|
||||
// the demo stubs are deliberately left unused in some tests
|
||||
}
|
||||
}
|
||||
|
||||
private MockitoSession strict() {
|
||||
return org.mockito.Mockito.mockitoSession().strictness(Strictness.STRICT_STUBS).startMocking();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unusedStubIsReportedWhenTheSessionEnds() {
|
||||
Transcript t = new Transcript("10-unnecessary-stubbing.txt");
|
||||
MockitoSession session = strict();
|
||||
PaymentGateway gateway = mock(PaymentGateway.class);
|
||||
when(gateway.isAvailable()).thenReturn(true); // used below
|
||||
when(gateway.charge("acc-1", 100)).thenReturn(null); // never used
|
||||
gateway.isAvailable();
|
||||
|
||||
UnnecessaryStubbingException e = assertThrows(UnnecessaryStubbingException.class, session::finishMocking);
|
||||
t.block("UnnecessaryStubbingException:", e.getMessage());
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameMethodDifferentArgumentsIsAPotentialStubbingProblem() {
|
||||
Transcript t = new Transcript("11-potential-stubbing-problem.txt");
|
||||
MockitoSession session = strict();
|
||||
try {
|
||||
PaymentGateway gateway = mock(PaymentGateway.class);
|
||||
when(gateway.isAvailable()).thenReturn(true);
|
||||
when(gateway.charge("acc-1", 900)).thenReturn(new Receipt("r", "acc-1", 900, true));
|
||||
CheckoutService svc = new CheckoutService(gateway, mock(AuditLog.class), new PriceCalculator());
|
||||
|
||||
// 1000 cents with 0% discount reaches charge("acc-1", 1000) inside CheckoutService.java,
|
||||
// a different source file from this test: Mockito flags the argument mismatch.
|
||||
PotentialStubbingProblem e = assertThrows(PotentialStubbingProblem.class, () -> svc.checkout("acc-1", 1000, 0));
|
||||
t.block("PotentialStubbingProblem:", e.getMessage());
|
||||
} finally {
|
||||
finish(session);
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameFileMismatchIsNotFlagged() {
|
||||
Transcript t = new Transcript("11b-same-file-mismatch-not-flagged.txt");
|
||||
MockitoSession session = strict();
|
||||
try {
|
||||
PaymentGateway gateway = mock(PaymentGateway.class);
|
||||
when(gateway.charge("acc-1", 900)).thenReturn(new Receipt("r", "acc-1", 900, true));
|
||||
// Same call-shape as the test above, but made from this very file.
|
||||
Receipt r = assertDoesNotThrow(() -> gateway.charge("acc-1", 1000));
|
||||
t.line("charge(\"acc-1\", 1000) from the test file returns : %s (no PotentialStubbingProblem)", r);
|
||||
assertNull(r);
|
||||
} finally {
|
||||
finish(session);
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lenientStubsAreExemptAndStrictnessCanBeLoosenedPerStub() {
|
||||
Transcript t = new Transcript("12-lenient.txt");
|
||||
MockitoSession session = strict();
|
||||
PaymentGateway gateway = mock(PaymentGateway.class);
|
||||
lenient().when(gateway.charge("acc-1", 100)).thenReturn(new Receipt("r", "acc-1", 100, true));
|
||||
lenient().when(gateway.isAvailable()).thenReturn(true);
|
||||
Receipt other = gateway.charge("acc-1", 999); // arg mismatch, but the stub is lenient
|
||||
t.line("lenient stub, mismatching call returns : %s", other);
|
||||
assertNull(other);
|
||||
session.finishMocking(); // unused lenient stub: no exception
|
||||
t.line("finishMocking with unused lenient stubs : no exception");
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainMockWithoutSessionIsLenient() {
|
||||
Transcript t = new Transcript("13-no-session-no-checks.txt");
|
||||
PaymentGateway gateway = mock(PaymentGateway.class);
|
||||
when(gateway.charge("acc-1", 100)).thenReturn(new Receipt("r", "acc-1", 100, true));
|
||||
Receipt other = gateway.charge("acc-1", 999);
|
||||
t.line("mismatching call on a plain mock returns : %s (no strictness error)", other);
|
||||
assertNull(other);
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package lab.mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Collects lines during a test and writes them to output/NN-name.txt.
|
||||
* The same test asserts the same facts, so every figure in the blog post is an assertion.
|
||||
*/
|
||||
final class Transcript {
|
||||
private final String file;
|
||||
private final List<String> lines = new ArrayList<>();
|
||||
|
||||
Transcript(String file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
Transcript line(String format, Object... args) {
|
||||
lines.add(args.length == 0 ? format : String.format(format, args));
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Message text with the volatile first-line class prefix kept, trailing whitespace trimmed. */
|
||||
Transcript block(String label, String text) {
|
||||
lines.add(label);
|
||||
for (String l : text.strip().split("\n")) {
|
||||
lines.add(" " + l.stripTrailing());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
void write() {
|
||||
try {
|
||||
Path dir = Path.of("output");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve(file), String.join("\n", lines) + "\n");
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user