Tại sao fixtureSetup của jUnit phải tĩnh?


109

Tôi đã đánh dấu một phương thức bằng chú thích @BeforeClass của jUnit và nhận được ngoại lệ này nói rằng nó phải tĩnh. Cơ sở lý luận là gì? Điều này buộc tất cả các init của tôi phải ở trên các trường tĩnh, không có lý do chính đáng theo như tôi thấy.

Trong .Net (NUnit), đây không phải là trường hợp.

Chỉnh sửa - thực tế là một phương thức được chú thích bằng @BeforeClass chỉ chạy một lần không liên quan gì đến việc nó là một phương thức tĩnh - người ta có thể có một phương thức không tĩnh chỉ chạy một lần (như trong NUnit).

Câu trả lời:


122

JUnit luôn tạo một phiên bản của lớp thử nghiệm cho mỗi phương thức @Test. Đây là một quyết định thiết kế cơ bản để giúp việc viết bài kiểm tra dễ dàng hơn mà không có tác dụng phụ. Các bài kiểm tra tốt không có bất kỳ phụ thuộc thứ tự chạy nào (xem FIRST ) và việc tạo các phiên bản mới của lớp kiểm tra và các biến cá thể của nó cho mỗi bài kiểm tra là rất quan trọng để đạt được điều này. Một số khuôn khổ kiểm tra sử dụng lại cùng một phiên bản lớp kiểm tra cho tất cả các bài kiểm tra, điều này dẫn đến nhiều khả năng vô tình tạo ra tác dụng phụ giữa các bài kiểm tra.

Và bởi vì mỗi phương pháp thử nghiệm có cá thể riêng của nó, nên không có ý nghĩa gì đối với các phương thức @ BeforeClass / @ AfterClass là phương thức cá thể. Nếu không, phương thức nào trong các trường hợp lớp thử nghiệm nên được gọi? Nếu các phương thức @ BeforeClass / @ AfterClass có thể tham chiếu đến các biến cá thể, thì chỉ một trong các phương thức @Test mới có quyền truy cập vào các biến cá thể đó - phần còn lại sẽ có các biến cá thể ở giá trị mặc định của chúng - và @ Phương thức kiểm tra sẽ được chọn ngẫu nhiên, vì thứ tự của các phương thức trong tệp .class là không xác định / phụ thuộc vào trình biên dịch (IIRC, API phản chiếu của Java trả về các phương thức theo thứ tự như chúng được khai báo trong tệp .class, mặc dù cũng có hành vi đó là không xác định - tôi đã viết một thư viện vì thực sự sắp xếp chúng theo số dòng của chúng).

Vì vậy, buộc các phương thức đó phải tĩnh là giải pháp hợp lý duy nhất.

Đây là một ví dụ:

public class ExampleTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("beforeClass");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("afterClass");
    }

    @Before
    public void before() {
        System.out.println(this + "\tbefore");
    }

    @After
    public void after() {
        System.out.println(this + "\tafter");
    }

    @Test
    public void test1() {
        System.out.println(this + "\ttest1");
    }

    @Test
    public void test2() {
        System.out.println(this + "\ttest2");
    }

    @Test
    public void test3() {
        System.out.println(this + "\ttest3");
    }
}

Bản in nào:

beforeClass
ExampleTest@3358fd70    before
ExampleTest@3358fd70    test1
ExampleTest@3358fd70    after
ExampleTest@6293068a    before
ExampleTest@6293068a    test2
ExampleTest@6293068a    after
ExampleTest@22928095    before
ExampleTest@22928095    test3
ExampleTest@22928095    after
afterClass

Như bạn có thể thấy, mỗi bài kiểm tra được thực hiện với phiên bản riêng của nó. Những gì JUnit làm về cơ bản giống như sau:

ExampleTest.beforeClass();

ExampleTest t1 = new ExampleTest();
t1.before();
t1.test1();
t1.after();

ExampleTest t2 = new ExampleTest();
t2.before();
t2.test2();
t2.after();

ExampleTest t3 = new ExampleTest();
t3.before();
t3.test3();
t3.after();

ExampleTest.afterClass();

1
"Nếu không, phương thức nào trong các trường hợp lớp thử nghiệm nên được gọi?" - Trên trường hợp thử nghiệm mà chạy thử nghiệm JUnit đã tạo để thực hiện các thử nghiệm.
HDave

1
Trong ví dụ đó, nó đã tạo ra ba trường hợp thử nghiệm. Không có phiên bản thử nghiệm.
Esko Luontola,

Có - tôi đã bỏ lỡ điều đó trong ví dụ của bạn. Tôi đã suy nghĩ nhiều hơn về thời điểm JUnit được gọi từ một thử nghiệm đang chạy Eclipse hoặc Spring Test, hoặc Maven. Trong những trường hợp đó, có một phiên bản của lớp thử nghiệm được tạo.
HDave

Không, JUnit luôn tạo ra rất nhiều phiên bản của lớp thử nghiệm, bất kể chúng tôi đã sử dụng những gì để khởi chạy thử nghiệm. Chỉ khi bạn có một Runner tùy chỉnh cho một lớp thử nghiệm thì điều gì đó khác có thể xảy ra.
Esko Luontola

Mặc dù tôi hiểu quyết định thiết kế, tôi nghĩ rằng quyết định đó không tính đến nhu cầu kinh doanh của người dùng. Vì vậy, cuối cùng, quyết định thiết kế nội bộ (điều mà tôi không nên quan tâm nhiều như một người dùng ngay khi lib hoạt động tốt) buộc tôi phải lựa chọn thiết kế trong các thử nghiệm của mình. Đó là thực sự không nhanh nhẹn ở tất cả: D
gicappa

43

Câu trả lời ngắn gọn là: không có lý do chính đáng nào để nó tĩnh.

Trên thực tế, làm cho nó tĩnh gây ra tất cả các loại vấn đề nếu bạn sử dụng Junit để thực hiện các bài kiểm tra tích hợp DAO dựa trên DBUnit. Yêu cầu tĩnh can thiệp vào quá trình chèn phụ thuộc, truy cập ngữ cảnh ứng dụng, xử lý tài nguyên, ghi nhật ký và bất kỳ thứ gì phụ thuộc vào "getClass".


4
Tôi đã viết superclass trường hợp thử nghiệm của riêng mình và sử dụng chú thích Spring @PostConstructđể thiết lập và @AfterClassgỡ bỏ và tôi bỏ qua hoàn toàn những cái tĩnh từ Junit. Đối với các bài kiểm tra DAO sau đó tôi đã viết TestCaseDataLoaderlớp của riêng mình mà tôi gọi ra từ các phương thức này.
HDave

9
Đó là một câu trả lời khủng khiếp, rõ ràng là trên thực tế, có lý do để nó tĩnh vì câu trả lời được chấp nhận đã chỉ ra rõ ràng. Bạn có thể không đồng ý với quyết định thiết kế, nhưng điều đó không có nghĩa là "không có lý do chính đáng" cho quyết định này.
Adam Parkin

8
Tất nhiên các tác giả JUnit có một lý do, tôi nói không phải là một mình tốt lý do ... do đó là nguồn gốc của OP (và 44 người khác) bị hoang mang. Sẽ là tầm thường nếu sử dụng các phương thức thể hiện và yêu cầu người chạy thử nghiệm sử dụng một quy ước để gọi chúng. Cuối cùng, đó là những gì mọi người làm để giải quyết hạn chế này - hoặc cuộn người chạy của riêng bạn hoặc cuộn lớp thử nghiệm của riêng bạn.
HDave

1
@HDave, tôi nghĩ rằng giải pháp của bạn với @PostConstruct@AfterClasschỉ hoạt động giống như @Before@After. Trên thực tế, các phương thức của bạn sẽ được gọi cho từng phương pháp thử nghiệm chứ không phải một lần cho toàn bộ lớp (như Esko Luontola đã nói trong câu trả lời của mình, một thể hiện của lớp được tạo cho mỗi phương pháp thử nghiệm). Tôi không thể nhìn thấy các tiện ích của giải pháp của bạn như vậy (trừ khi tôi bỏ lỡ một cái gì đó)
magnum87

1
Nó đã chạy chính xác trong 5 năm nay, vì vậy tôi nghĩ rằng giải pháp của tôi hoạt động.
HDave

13

Tài liệu về JUnit có vẻ khan hiếm, nhưng tôi đoán: có lẽ JUnit tạo một phiên bản mới của lớp thử nghiệm của bạn trước khi chạy mỗi trường hợp thử nghiệm, vì vậy cách duy nhất để trạng thái "cố định" của bạn tồn tại qua các lần chạy là đặt nó ở trạng thái tĩnh, có thể được thực thi bằng cách đảm bảo fixtureSetup (phương thức @BeforeClass) của bạn là tĩnh.


2
Không chỉ có lẽ, mà JUnit chắc chắn tạo ra một trường hợp thử nghiệm mới. Vì vậy, đây là lý do duy nhất.
evalda

Đây là lý do duy nhất mà họ có, nhưng thực tế thì trình chạy Junit có thể thực hiện công việc thực thi các phương thức BeforeTests và AfterTests theo cách testng.
HDave

TestNG có tạo một phiên bản của lớp kiểm tra và chia sẻ nó với tất cả các bài kiểm tra trong lớp không? Điều đó làm cho nó dễ bị tác dụng phụ hơn giữa các lần kiểm tra.
Esko Luontola

3

Mặc dù điều này sẽ không trả lời câu hỏi ban đầu. Nó sẽ trả lời rõ ràng theo dõi. Cách tạo quy tắc hoạt động trước và sau một lớp học và trước và sau một bài kiểm tra.

Để đạt được điều đó, bạn có thể sử dụng mẫu này:

@ClassRule
public static JPAConnection jpaConnection = JPAConnection.forUITest("my-persistence-unit");

@Rule
public JPAConnection.EntityManager entityManager = jpaConnection.getEntityManager();

Vào trước (Lớp), JPAConnection tạo kết nối một lần vào sau (Lớp) nó đóng nó.

getEntityMangertrả về một lớp bên trong JPAConnectionthực hiện EntityManager của jpa và có thể truy cập kết nối bên trong jpaConnection. Vào trước khi (thử nghiệm), nó bắt đầu một giao dịch vào sau (thử nghiệm), nó sẽ quay trở lại.

Điều này không an toàn cho luồng nhưng có thể được thực hiện như vậy.

Mã đã chọn của JPAConnection.class

package com.triodos.general.junit;

import com.triodos.log.Logger;
import org.jetbrains.annotations.NotNull;
import org.junit.rules.ExternalResource;

import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.Metamodel;
import java.util.HashMap;
import java.util.Map;

import static com.google.common.base.Preconditions.checkState;
import static com.triodos.dbconn.DB2DriverManager.DRIVERNAME_TYPE4;
import static com.triodos.dbconn.UnitTestProperties.getDatabaseConnectionProperties;
import static com.triodos.dbconn.UnitTestProperties.getPassword;
import static com.triodos.dbconn.UnitTestProperties.getUsername;
import static java.lang.String.valueOf;
import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

public final class JPAConnectionExample extends ExternalResource {

  private static final Logger LOG = Logger.getLogger(JPAConnectionExample.class);

  @NotNull
  public static JPAConnectionExample forUITest(String persistenceUnitName) {
    return new JPAConnectionExample(persistenceUnitName)
        .setManualEntityManager();
  }

  private final String persistenceUnitName;
  private EntityManagerFactory entityManagerFactory;
  private javax.persistence.EntityManager jpaEntityManager = null;
  private EntityManager entityManager;

  private JPAConnectionExample(String persistenceUnitName) {
    this.persistenceUnitName = persistenceUnitName;
  }

  @NotNull
  private JPAConnectionExample setEntityManager(EntityManager entityManager) {
    this.entityManager = entityManager;
    return this;
  }

  @NotNull
  private JPAConnectionExample setManualEntityManager() {
    return setEntityManager(new RollBackAfterTestEntityManager());
  }


  @Override
  protected void before() {
    entityManagerFactory = Persistence.createEntityManagerFactory(persistenceUnitName, createEntityManagerProperties());
    jpaEntityManager = entityManagerFactory.createEntityManager();
  }

  @Override
  protected void after() {

    if (jpaEntityManager.getTransaction().isActive()) {
      jpaEntityManager.getTransaction().rollback();
    }

    if(jpaEntityManager.isOpen()) {
      jpaEntityManager.close();
    }
    // Free for garbage collection as an instance
    // of EntityManager may be assigned to a static variable
    jpaEntityManager = null;

    entityManagerFactory.close();
    // Free for garbage collection as an instance
    // of JPAConnection may be assigned to a static variable
    entityManagerFactory = null;
  }

  private Map<String,String> createEntityManagerProperties(){
    Map<String, String> properties = new HashMap<>();
    properties.put("javax.persistence.jdbc.url", getDatabaseConnectionProperties().getURL());
    properties.put("javax.persistence.jtaDataSource", null);
    properties.put("hibernate.connection.isolation", valueOf(TRANSACTION_READ_UNCOMMITTED));
    properties.put("hibernate.connection.username", getUsername());
    properties.put("hibernate.connection.password", getPassword());
    properties.put("hibernate.connection.driver_class", DRIVERNAME_TYPE4);
    properties.put("org.hibernate.readOnly", valueOf(true));

    return properties;
  }

  @NotNull
  public EntityManager getEntityManager(){
    checkState(entityManager != null);
    return entityManager;
  }


  private final class RollBackAfterTestEntityManager extends EntityManager {

    @Override
    protected void before() throws Throwable {
      super.before();
      jpaEntityManager.getTransaction().begin();
    }

    @Override
    protected void after() {
      super.after();

      if (jpaEntityManager.getTransaction().isActive()) {
        jpaEntityManager.getTransaction().rollback();
      }
    }
  }

  public abstract class EntityManager extends ExternalResource implements javax.persistence.EntityManager {

    @Override
    protected void before() throws Throwable {
      checkState(jpaEntityManager != null, "JPAConnection was not initialized. Is it a @ClassRule? Did the test runner invoke the rule?");

      // Safety-close, if failed to close in setup
      if (jpaEntityManager.getTransaction().isActive()) {
        jpaEntityManager.getTransaction().rollback();
        LOG.error("EntityManager encountered an open transaction at the start of a test. Transaction has been closed but should have been closed in the setup method");
      }
    }

    @Override
    protected void after() {
      checkState(jpaEntityManager != null, "JPAConnection was not initialized. Is it a @ClassRule? Did the test runner invoke the rule?");
    }

    @Override
    public final void persist(Object entity) {
      jpaEntityManager.persist(entity);
    }

    @Override
    public final <T> T merge(T entity) {
      return jpaEntityManager.merge(entity);
    }

    @Override
    public final void remove(Object entity) {
      jpaEntityManager.remove(entity);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey) {
      return jpaEntityManager.find(entityClass, primaryKey);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties) {
      return jpaEntityManager.find(entityClass, primaryKey, properties);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode) {
      return jpaEntityManager.find(entityClass, primaryKey, lockMode);
    }

    @Override
    public final <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode, Map<String, Object> properties) {
      return jpaEntityManager.find(entityClass, primaryKey, lockMode, properties);
    }

    @Override
    public final <T> T getReference(Class<T> entityClass, Object primaryKey) {
      return jpaEntityManager.getReference(entityClass, primaryKey);
    }

    @Override
    public final void flush() {
      jpaEntityManager.flush();
    }

    @Override
    public final void setFlushMode(FlushModeType flushMode) {
      jpaEntityManager.setFlushMode(flushMode);
    }

    @Override
    public final FlushModeType getFlushMode() {
      return jpaEntityManager.getFlushMode();
    }

    @Override
    public final void lock(Object entity, LockModeType lockMode) {
      jpaEntityManager.lock(entity, lockMode);
    }

    @Override
    public final void lock(Object entity, LockModeType lockMode, Map<String, Object> properties) {
      jpaEntityManager.lock(entity, lockMode, properties);
    }

    @Override
    public final void refresh(Object entity) {
      jpaEntityManager.refresh(entity);
    }

    @Override
    public final void refresh(Object entity, Map<String, Object> properties) {
      jpaEntityManager.refresh(entity, properties);
    }

    @Override
    public final void refresh(Object entity, LockModeType lockMode) {
      jpaEntityManager.refresh(entity, lockMode);
    }

    @Override
    public final void refresh(Object entity, LockModeType lockMode, Map<String, Object> properties) {
      jpaEntityManager.refresh(entity, lockMode, properties);
    }

    @Override
    public final void clear() {
      jpaEntityManager.clear();
    }

    @Override
    public final void detach(Object entity) {
      jpaEntityManager.detach(entity);
    }

    @Override
    public final boolean contains(Object entity) {
      return jpaEntityManager.contains(entity);
    }

    @Override
    public final LockModeType getLockMode(Object entity) {
      return jpaEntityManager.getLockMode(entity);
    }

    @Override
    public final void setProperty(String propertyName, Object value) {
      jpaEntityManager.setProperty(propertyName, value);
    }

    @Override
    public final Map<String, Object> getProperties() {
      return jpaEntityManager.getProperties();
    }

    @Override
    public final Query createQuery(String qlString) {
      return jpaEntityManager.createQuery(qlString);
    }

    @Override
    public final <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery) {
      return jpaEntityManager.createQuery(criteriaQuery);
    }

    @Override
    public final <T> TypedQuery<T> createQuery(String qlString, Class<T> resultClass) {
      return jpaEntityManager.createQuery(qlString, resultClass);
    }

    @Override
    public final Query createNamedQuery(String name) {
      return jpaEntityManager.createNamedQuery(name);
    }

    @Override
    public final <T> TypedQuery<T> createNamedQuery(String name, Class<T> resultClass) {
      return jpaEntityManager.createNamedQuery(name, resultClass);
    }

    @Override
    public final Query createNativeQuery(String sqlString) {
      return jpaEntityManager.createNativeQuery(sqlString);
    }

    @Override
    public final Query createNativeQuery(String sqlString, Class resultClass) {
      return jpaEntityManager.createNativeQuery(sqlString, resultClass);
    }

    @Override
    public final Query createNativeQuery(String sqlString, String resultSetMapping) {
      return jpaEntityManager.createNativeQuery(sqlString, resultSetMapping);
    }

    @Override
    public final void joinTransaction() {
      jpaEntityManager.joinTransaction();
    }

    @Override
    public final <T> T unwrap(Class<T> cls) {
      return jpaEntityManager.unwrap(cls);
    }

    @Override
    public final Object getDelegate() {
      return jpaEntityManager.getDelegate();
    }

    @Override
    public final void close() {
      jpaEntityManager.close();
    }

    @Override
    public final boolean isOpen() {
      return jpaEntityManager.isOpen();
    }

    @Override
    public final EntityTransaction getTransaction() {
      return jpaEntityManager.getTransaction();
    }

    @Override
    public final EntityManagerFactory getEntityManagerFactory() {
      return jpaEntityManager.getEntityManagerFactory();
    }

    @Override
    public final CriteriaBuilder getCriteriaBuilder() {
      return jpaEntityManager.getCriteriaBuilder();
    }

    @Override
    public final Metamodel getMetamodel() {
      return jpaEntityManager.getMetamodel();
    }
  }
}

2

Có vẻ như JUnit tạo một phiên bản mới của lớp thử nghiệm cho mỗi phương pháp thử nghiệm. Hãy thử mã này ra

public class TestJunit
{

    int count = 0;

    @Test
    public void testInc1(){
        System.out.println(count++);
    }

    @Test
    public void testInc2(){
        System.out.println(count++);
    }

    @Test
    public void testInc3(){
        System.out.println(count++);
    }
}

Đầu ra là 0 0 0

Điều này có nghĩa là nếu phương thức @BeforeClass không phải là tĩnh thì nó sẽ phải được thực thi trước mỗi phương thức kiểm tra và sẽ không có cách nào để phân biệt giữa ngữ nghĩa của @Before và @BeforeClass


Nó không chỉ có vẻ như vậy, nó như vậy. Câu hỏi đã được đặt ra trong nhiều năm, đây là câu trả lời: martinfowler.com/bliki/JunitNewInstance.html
Paul

1

có hai loại chú thích:

  • @BeforeClass (@AfterClass) được gọi một lần cho mỗi lớp thử nghiệm
  • @Before (và @After) được gọi trước mỗi bài kiểm tra

vì vậy @BeforeClass phải được khai báo tĩnh vì nó được gọi một lần. Bạn cũng nên xem xét rằng tĩnh là cách duy nhất để đảm bảo truyền "trạng thái" thích hợp giữa các thử nghiệm (mô hình JUnit áp đặt một trường hợp thử nghiệm cho mỗi @Test) và, vì trong Java chỉ có các phương thức tĩnh mới có thể truy cập dữ liệu tĩnh ... @BeforeClass và @ AfterClass chỉ có thể được áp dụng cho các phương thức tĩnh.

Kiểm tra ví dụ này sẽ làm rõ việc sử dụng @BeforeClass và @Before:

public class OrderTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("before class");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("after class");
    }

    @Before
    public void before() {
        System.out.println("before");
    }

    @After
    public void after() {
        System.out.println("after");
    }    

    @Test
    public void test1() {
        System.out.println("test 1");
    }

    @Test
    public void test2() {
        System.out.println("test 2");
    }
}

đầu ra:

------------- Đầu ra chuẩn ---------------
trước lớp
trước
kiểm tra 1
sau
trước
kiểm tra 2
sau
sau giờ học
------------- ---------------- ---------------

19
Tôi thấy câu trả lời của bạn không liên quan. Tôi biết ngữ nghĩa của BeforeClass và Before. Điều này không giải thích lý do tại sao nó phải được tĩnh ...
ripper234

1
"Điều này buộc tất cả các init của tôi phải ở trên các thành viên tĩnh, không có lý do chính đáng theo như tôi thấy." Câu trả lời của tôi nên cho bạn thấy rằng init của bạn có thể cũng không tĩnh sử dụng @Before, thay vì @BeforeClass
DFA

2
Tôi chỉ muốn thực hiện một số init một lần duy nhất ở đầu lớp, nhưng trên các biến không tĩnh.
ripper234

bạn không thể với JUnit, xin lỗi. Bạn phải sử dụng một biến tĩnh, không thể nào.
dfa

1
Nếu khởi tạo là tốn kém, bạn chỉ có thể giữ một biến trạng thái để ghi cho dù bạn đã thực hiện các init, và (kiểm tra xem nó, và tùy chọn) thực hiện các init trong một phương pháp @Before ...
Blair Conrad

0

Theo JUnit 5, có vẻ như triết lý về việc tạo ra một phiên bản mới cho mỗi phương pháp thử nghiệm đã được nới lỏng phần nào. Họ đã thêm một chú thích sẽ khởi tạo một lớp thử nghiệm chỉ một lần. Chú thích này do đó cũng cho phép các phương thức được chú thích bằng @ BeforeAll / @ AfterAll (thay thế cho @ BeforeClass / @ AfterClass) là không tĩnh. Vì vậy, một lớp thử nghiệm như thế này:

@TestInstance(Lifecycle.PER_CLASS)
class TestClass() {
    Object object;

    @BeforeAll
    void beforeAll() {
        object = new Object();
    }

    @Test
    void testOne() {
        System.out.println(object);
    }

    @Test
    void testTwo() {
        System.out.println(object);
    }
}

sẽ in:

java.lang.Object@799d4f69
java.lang.Object@799d4f69

Vì vậy, bạn thực sự có thể khởi tạo các đối tượng một lần cho mỗi lớp thử nghiệm. Tất nhiên, điều này khiến bạn có trách nhiệm tránh các đối tượng đột biến được khởi tạo theo cách này.


-11

Để giải quyết vấn đề này, chỉ cần thay đổi phương pháp

public void setUpBeforeClass 

đến

public static void setUpBeforeClass()

và tất cả những gì được định nghĩa trong phương pháp này để static.


2
Điều này không trả lời câu hỏi nào cả.
rgargente,
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.