Тесты JUnit проходят только тогда, когда я запускаю их один за другим. Вероятно, неправильная обработка потоков

#java #spring #multithreading #spring-boot #junit

#java #весна #многопоточность #весенняя загрузка #junit

Вопрос:

UPD: Отмеченный ответ решил проблему, См. Его Комментарии для небольших исправлений.

Прежде всего, простите меня за вырезанный код. Эта проблема сводит меня с ума, я перепробовал много вещей, чтобы определить источник проблемы, но потерпел неудачу.

У меня есть веб-приложение Spring Boot, в котором есть служба FMonitor. Он работает во время работы приложения и уведомляет меня, когда в определенной папке появляется файл с расширением «.done». Когда я запускаю приложение, оно работает безупречно (по крайней мере, так кажется).

Я решил потренироваться с тестами и бросил три простых. Они успешно работают, когда я запускаю их по одному. Однако, когда я запускаю весь класс тестов, проходит только первый, два других завершаются неудачей.

Мои тесты проверяют System.out и сравнивают его с желаемым результатом. И вот самая странная вещь для меня. Вот что происходит.

Первый тест проходит, вывод FMonitor точно такой же, как в assertEquals. Найдено test1
Найдено test2
Найдено test3

Второй тест завершается неудачей. По какой-то причине вывод странно удваивается: Найдено test1
Найдено test1
Найдено test3
Найдено test3

А затем третий сбой. Теперь вывод утроен: найден тест
, найден тест
, найден тест

Я предполагаю, что я делаю что-то совершенно неправильное с потоками, поэтому fm.monitor() каким-то образом улавливает все события и что-то в этом роде. Я очень смущен. Я много чего перепробовал с тем, как реализовать здесь потоковую обработку, я не очень в этом разбираюсь, но все равно работает одинаково. Также я подумал, что @Async аннотация для monitor() может что-то испортить, но ее удаление ничего не изменило. Помогите, пожалуйста.

BunchOfTests

 import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.*;
import java.util.concurrent.Executor;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = FMonitor.class)
public class BunchOfTests {
    private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
    private Executor executor;

    @Autowired
    FMonitor fm;

    @Test
    public void test1() throws InterruptedException, IOException {
        Runnable task = () -> {
            System.setOut(new PrintStream(outContent));
            fm.monitor();
            System.setOut(System.out);
        };
        executor = (runnable) -> new Thread(runnable).start();
        executor.executeTask(task);
        Thread.sleep(3000);
        File file1 = new File("C:\dir\test1.done");
        File file2 = new File("C:\dir\test2.done");
        File file3 = new File("C:\dir\test3.done");
        file1.createNewFile();
        file2.createNewFile();
        file3.createNewFile();
        Thread.sleep(3000);
        file1.delete();
        file2.delete();
        file3.delete();
        Assert.assertEquals("Found test1rn"   "Found test2rn"   "Found test3rn", outContent);
    }

    @Test
    public void test2() throws InterruptedException, IOException {
        Runnable task = () -> {
            System.setOut(new PrintStream(outContent));
            fm.monitor();
            System.setOut(System.out);
        };
        executor = (runnable) -> new Thread(runnable).start();
        executor.executeTask(task);
        Thread.sleep(3000);
        File file1 = new File("C:\dir\test1.done");
        File file2 = new File("C:\dir\test2.txt");
        File file3 = new File("C:\dir\test3.done");
        file1.createNewFile();
        file2.createNewFile();
        file3.createNewFile();
        Thread.sleep(3000);
        file1.delete();
        file2.delete();
        file3.delete();
        Assert.assertEquals("Found test1rn"   "Found test3rn", outContent);
    }

    @Test
    public void test3() throws InterruptedException, IOException {
        Runnable task = () -> {
            System.setOut(new PrintStream(outContent));
            fm.monitor();
            System.setOut(System.out);
        };
        executor = (runnable) -> new Thread(runnable).start();
        executor.executeTask(task);
        Thread.sleep(3000);
        File file = new File("C:\dir\test.done");
        file.createNewFile();
        Thread.sleep(3000);
        file.delete();
        Assert.assertEquals("Found testrn", outContent);
    }
}
 

FMonitor

 import org.springframework.stereotype.Service;
import org.srpingframework.scheduling.annotation.Async;

import java.io.IOException;
import java.nio.file.*;

@Service
public class FMonitor {

    @Async("fMonitor")
    public void monitor() {
        Path path = Paths.get("C:\dir");
        try {
            WatchService watchService = FileSystems.getDefault.newWatchService();
            path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
            WatchKey key;
            while ((key = watchService.take()) != null) {
                for (WatchEvent<?> event: key.pollEvents()) {
                    String filename = event.context().toString();
                    if (filename.endsWith(".done")) {
                        processFile(filename.substring(0, filename.lastIndexOf('.')));
                    }
                }
                key.reset();
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void processFile(String filename) {
        System.out.println("Found "   filename);
    }
}
 

Конфигурация

 import org.srpingframework.context.annotation.Bean;
import org.srpingframework.context.annotation.Configuration;
import org.srpingframework.scheduling.annotation.EnableAsync;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Configuration
@EnableAsync
public class AConfiguration {
    @Bean(name = "fMonitor")
    public Executor asyncExecutor() { return Executors.newSingleThreadExecutor(); }
}
 

Комментарии:

1. Используйте правильный исполнитель и после завершения теста завершите его, чтобы потоки были остановлены.

2. @M.Deinum Я пробовал это, не помогло

3. Вы пробовали что? Я также не совсем понимаю ваш тест. Почему вы выполняете задачу в исполнителе, в то время как Spring Boot уже выполняет эту задачу в потоке…. Каждый раз, когда вы вызываете monitor , он запускает новый. Добавление одного в список задач, потому что есть только 1 приложение. Таким образом, вы получаете несколько задач, по 1 для каждого теста. ИТАК, первая выполняет 1 задачу, вторая — 2 задачи, а третья — 3 задачи. И вам не нужно Executor в вашем тесте.

4. @M.Deinum Я попытался использовать «правильный исполнитель с завершением работы». ExecutorService. Если вы знаете, как это сделать правильно, не стесняйтесь поделиться.

5. Как указано, проблема в том, что вы вызываете метод в управляемом компоненте spring. При каждом вызове monitor он будет планировать новую задачу. Каждый тест добавляет один. Вам не нужен исполнитель. Я бы сказал, что вы FMonitor немного ошибаетесь. Вы должны сохранить WatchService как переменную экземпляра и добавить метод с аннотацией @PreDestroy . После теста вызовите этот метод для очистки наблюдателя.

Ответ №1:

Для начала вы должны аккуратно остановить WatchService , когда закончите. Реализуйте метод, который делает это, и аннотируйте с @PreDestroy помощью .

 @Service
public class FMonitor {

    private final WatchService watchService = FileSystems.getDefault.newWatchService();


    @Async("fMonitor")
    public void monitor() {
        Path path = Paths.get("C:\dir");
        try {
            path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
            WatchKey key;
            while ((key = watchService.take()) != null) {
                for (WatchEvent<?> event: key.pollEvents()) {
                    String filename = event.context().toString();
                    if (filename.endsWith(".done")) {
                        processFile(filename.substring(0, filename.lastIndexOf('.')));
                    }
                }
                key.reset();
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void processFile(String filename) {
        System.out.println("Found "   filename);
    }

    @PreDestroy
    public void shutdown() {
      try {
        watchService.close();
      } catch (IOException ex) {}
    }
}
 

Далее не используйте @SpringBootTest , это не добавляет к вашему тесту ничего, кроме сложности и медлительности. Вместо этого просто создайте его самостоятельно и используйте соответствующий исполнитель для запуска monitor метода.

 public class BunchOfTests {

    @Rule
    public OutputCaptureRule output = new OutputCaptureRule();
    
    private ExecutorService executor = Executors.newSingleThreadExecutor();

    private final FMonitor fm = new FMonitor();

    @After
    public void cleanUp() throws Exception {
      fm.shutdown();
      executor.shutdown();
      while (!executor.awaitTermination(100, TimeUnit.MICROSECONDS));
    }

    @Test
    public void test1() throws InterruptedException, IOException {
        executor.submit(() -> fm.monitor());

        Thread.sleep(3000);
        File file1 = new File("C:\dir\test1.done");
        File file2 = new File("C:\dir\test2.done");
        File file3 = new File("C:\dir\test3.done");
        file1.createNewFile();
        file2.createNewFile();
        file3.createNewFile();

        Thread.sleep(3000);
        file1.delete();
        file2.delete();
        file3.delete();
        Assert.assertEquals("Found test1rn"   "Found test2rn"   "Found test3rn", output.toString());
    }

    @Test
    public void test2() throws InterruptedException, IOException {
        executor.submit(() -> fm.monitor());

        Thread.sleep(3000);
        File file1 = new File("C:\dir\test1.done");
        File file2 = new File("C:\dir\test2.txt");
        File file3 = new File("C:\dir\test3.done");
        file1.createNewFile();
        file2.createNewFile();
        file3.createNewFile();

        Thread.sleep(3000);
        file1.delete();
        file2.delete();
        file3.delete();
        Assert.assertEquals("Found test1rn"   "Found test3rn", output.toString());
    }

    @Test
    public void test3() throws InterruptedException, IOException {
        executor.submit(() -> fm.monitor());

        Thread.sleep(3000);

        File file = new File("C:\dir\test.done");
        file.createNewFile();

        Thread.sleep(3000);
        file.delete();
        Assert.assertEquals("Found testrn", output.toString());
    }
}
 

Что-то подобное должно более или менее делать то, что вы хотите.

Комментарии:

1. Большое вам спасибо. Это помогло. Существует опечатка, вы забыли «do { /code/ }» для do во время построения в @After тестового класса. Также для assertEquals я изменил простой «вывод» на «вывод». toString()». На случай, если кто-то попытается запустить ваш пример

2. Пропущенных действий нет. Это просто пустой цикл while, который будет ждать завершения работы исполнителя.