-Поиск по дневнику

Поиск сообщений в rss_rss_hh_new

 -Подписка по e-mail

 

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 51

Habrahabr/New








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/new/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-new-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

Создание приложений с использованием Firebird, jOOQ и Spring MVC

Вторник, 27 Июня 2017 г. 09:37 + в цитатник
Всем привет. На этот раз будет описан процесс создания web приложения на языке Java с использованием фреймворка Spring MVC, библиотеки jOOQ и СУБД Firebird.

Для упрощения разработки вы можете воспользоваться одной из распространённых IDE для Java (NetBeans, IntelliJ IDEA, Eclipse, JDeveloper или др.). Лично я использовал NetBeans. Для тестирования и отладки нам так же потребуется установить один и веб-серверов или серверов приложения (Apache Tomcat или Glass Fish) Создаём проект на основе шаблона Maven проекта веб-приложения.


После создания проекта на основе шаблона необходимо преобразовать его структуру папок так чтобы она была корректной для MVC Spring 4. Если проект создавался в среде NetBeans 8.2, то необходимо выполнить следующие шаги:
  1. Удалить файл index.html
  2. Создать папку WEB-INF внутри папки Web Pages
  3. Внутри папки WEB-INF создать папки jsp, jspf и resources
  4. Внутри папки resources создаём папки js и CSS
  5. Внутри папки jsp создаём файл index.jsp


После наших манипуляций структура папок должна выглядеть следующим образом.



В папке WEB-INF/jsp будут размещаться jsp странице, а в папке jspf части страниц, которые будут подключены в другие странице с помощью инструкции



Папка resource предназначена для размещения статических веб ресурсов. В папке WEB-INF/resources/css будут размещаться файлы каскадных таблиц стилей, в папке WEB-INF/resources/fonts – файлы шрифтов, в папке WEB-INF/resources/js – файлы JavaScript и сторонние JavaScript библиотеки.

Теперь поправим файл pom.xml и пропишем в него общие свойства приложения, зависимости от пакетов библиотек (Spring MVC, Jaybird, JDBC пул, JOOQ) и свойства JDBC подключения.

pom.xml


    4.0.0

    ru.ibase
    fbjavaex
    1.0-SNAPSHOT
    war

    Firebird Java Example

    
        ${project.build.directory}/endorsed
        UTF-8
        4.3.4.RELEASE
        1.2
        3.0.1    
        jdbc:firebirdsql://localhost:3050/examples    
        org.firebirdsql.jdbc.FBDriver
        SYSDBA
        masterkey
    
    
    
    
        
            javax
            javaee-web-api
            7.0
            provided
        
        
          
            javax.servlet  
            javax.servlet-api  
            ${javax.servlet.version}  
            provided 
         
         
          
            jstl  
            jstl  
            ${jstl.version}  
           
        
        
        
            com.fasterxml.jackson.core
            jackson-core
            2.8.5
        

        
            com.fasterxml.jackson.core
            jackson-annotations
            2.8.5
        

        
            com.fasterxml.jackson.core
            jackson-databind
            2.8.5
                
                   
        
        
        
        
            org.springframework
            spring-core
            ${spring.version}
        
 
        
            org.springframework
            spring-web
            ${spring.version}
        
 
        
            org.springframework
            spring-webmvc
            ${spring.version}
        
        
        
            org.springframework
            spring-context
            ${spring.version}
        
    
        
            org.springframework
            spring-jdbc
            ${spring.version}
                
        
        
        
        
            org.firebirdsql.jdbc
            jaybird-jdk18
            3.0.0
          
        
          
        
        
            commons-dbcp
            commons-dbcp
            1.4
                  
        
           
                
        
            org.jooq
            jooq
            3.9.2
        
        
        
            org.jooq
            jooq-meta
            3.9.2
        
        
        
            org.jooq
            jooq-codegen
            3.9.2
           
        
        
        
            junit
            junit
            4.11
            jar
            test
        
    
        
            org.springframework
            spring-test
            ${spring.version}
            test
                               
    

                      
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.1
                
                    1.7
                    1.7
                    
                        ${endorsed.dir}
                    
                
            
            
                org.apache.maven.plugins
                maven-war-plugin
                2.3
                
                    false
                
            
            
                org.apache.maven.plugins
                maven-dependency-plugin
                2.6
                
                    
                        validate
                        
                            copy
                        
                        
                            ${endorsed.dir}
                            true
                            
                                
                                    javax
                                    javaee-endorsed-api
                                    7.0
                                    jar
                                
                            
                        
                    
                
            
        
    




После того как вы прописали все необходимые зависимости, желательно перезагрузить POM, чтобы загрузить все необходимые библиотеки. Если этого не сделать, то в процессе работы с проектом могут возникать ошибки. В NetBeans это делается следующим образом



Мне не очень нравится конфигурирование через xml, поэтому я буду работать через классы конфигурации Java.

WebAppConfig.java
package ru.ibase.fbjavaex.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.List;

@Configuration
@ComponentScan("ru.ibase.fbjavaex")
@EnableWebMvc
public class WebAppConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List> httpMessageConverters) {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        jsonConverter.setObjectMapper(objectMapper);
        httpMessageConverters.add(jsonConverter);
    }

    @Bean
    public UrlBasedViewResolver setupViewResolver() {
        UrlBasedViewResolver resolver = new UrlBasedViewResolver();
        resolver.setPrefix("/WEB-INF/jsp/");
        resolver.setSuffix(".jsp");
        resolver.setViewClass(JstlView.class);
        return resolver;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/WEB-INF/resources/");
    }
}



В данном конфигурационном классе мы задаём место поиска веб ресурсов и JSP представлений. Метод configureMessageConverters устанавливает, что дата должна сериализоваться в строковое представление (по умолчанию сериализуется в числовом представлении как timestamp).

Теперь избавимся от файла Web.xml вместо него создадим файл WebInitializer.java.

WebInitializer.java
package ru.ibase.fbjavaex.config;

import javax.servlet.ServletContext;  
import javax.servlet.ServletException;  
import javax.servlet.ServletRegistration.Dynamic;  
  
import org.springframework.web.WebApplicationInitializer;  
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;  
import org.springframework.web.servlet.DispatcherServlet;  
  
public class WebInitializer implements WebApplicationInitializer {
    
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {        
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();  
        ctx.register(WebAppConfig.class);  
        ctx.setServletContext(servletContext);    
        Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));  
        servlet.addMapping("/");  
        servlet.setLoadOnStartup(1);
    }
    
}



Осталось сконфигурировать IoC контейнеры для внедрения зависимостей. К этому шагу мы вернёмся позже, а сейчас перейдём к генерации классов для работы с базой данных через jOOQ.

Генерации классов для работы с базой данных через jOOQ



Работу с базой данных будем вести с помощью библиотеки jOOQ. jOOQ позволяет строить SQL запросы из объектов jOOQ и кода (наподобие LINQ). jOOQ имеет более тесную интеграцию с базой данных, чем ORM, поэтому кроме простых CRUD SQL запросов используемых в Active Record, позволяет использовать дополнительные возможности. Например, jOOQ умеет работать с хранимыми процедурами и функциями, последовательностями, использовать оконные функции и другие, специфичные для определённой СУБД, возможности. Полная документация по работе с jOOQ находится по ссылке.

Классы jOOQ для работы с базой данных генерируются на основе схемы базы данных. Наше приложение будет работать с базой данных, модель которой представлена на рисунке ниже.

image

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

Внимание!

Эта модель является просто примером. Ваша предметная область может быть сложнее, или полностью другой. Модель, используемая в этой статье, максимально упрощена для того, чтобы не загромождать описание работы с компонентами описанием создания и модификации модели данных.


Для генерации классов jOOQ, работающих с нашей БД, необходимо скачать следующие бинарные файлы по ссылке или через maven репозиторий:
  • jooq-3.9.2.jar
    Главная библиотека, которая включается в наше приложение для работы с jOOQ.
  • jooq-meta-3.9.2.jar
    Утилита, которая включается в вашу сборку для навигации по схеме базы данных через сгенерированные объекты.
  • jooq-codegen-3.9.2.jar
    Утилита, которая включается в вашу сборку для генерации схемы базы данных.


Кроме того для подключения к БД Firebird через JDBC вам потребуется скачать драйвер jaybird-full-3.0.0.jar.

Теперь надо создать файл конфигурации example.xml, который будет использован для генерации классов схемы БД.

example.xml


  
  
    org.firebirdsql.jdbc.FBDriver
    jdbc:firebirdsql://localhost:3050/examples
    SYSDBA
    masterkey
    
      
        charSet
        utf-8
      
    
  

  
    org.jooq.util.JavaGenerator

    
      
      org.jooq.util.firebird.FirebirdDatabase

      

      
      .*

      
      
	   RDB\$.*
	 | MON\$.*
	 | SEC\$.*
      
    

    
      
      ru.ibase.fbjavaex.exampledb

      
      e:/OpenServer/domains/localhost/fbjavaex/src/main/java/
    
  




Теперь переходим в командную строку и выполняем следующую команду:
java -cp jooq-3.9.2.jar;jooq-meta-3.9.2.jar;jooq-codegen-3.9.2.jar;jaybird-full-3.0.0.jar;. org.jooq.util.GenerationTool example.xml


Данная команда создаст необходимые классы и позволит писать на языке Java запросы к объектам БД. Подробнее с процессом генерации классов вы можете ознакомиться по ссылке Code generation.

Конфигурация IoC контейнеров



В Spring внедрение зависимостей (Dependency Injection (DI)) осуществляется через Spring IoC (Inversion of Control) контейнер. Внедрение зависимостей, является процессом, согласно которому объекты определяют свои зависимости, т.е. объекты, с которыми они работают, через аргументы конструктора/фабричного метода или свойства, которые были установлены или возвращены фабричным методом. Затем контейнер inject(далее «внедряет») эти зависимости при создании бина. Подробнее о внедрении зависимостей вы можете ознакомиться в главе The IoC container.

Я не сторонник xml конфигурации, поэтому мы будем использовать подход на основе аннотаций и Java-конфигурации. Основными признаками и частями Java-конфигурации IoC контейнера являются классы с аннотацией @Configuration и методы с аннотацией Bean. Аннотация Bean используется для указания того, что метод создает, настраивает и инициализирует новый объект, управляемый Spring IoC контейнером. Такие методы можно использовать как в классах с аннотацией @Configuration. Наш IoC контейнер будет возвращать пул подключений, менеджер транзакций, транслятор исключений (преобразует исключения SQLException в специфичные для Spring исключения DataAccessException), DSL контекст (стартовая точка, для построения всех запросов используя Fluent API), а также менеджеры для реализации бизнес логики и гриды для отображения данных.
JooqConfig.java
/**
 * Конфигурация IoC контейнера
 * для осуществления внедрения зависимостей.
 */

package ru.ibase.fbjavaex.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.impl.DefaultDSLContext;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.SQLDialect;
import org.jooq.impl.DefaultExecuteListenerProvider;

import ru.ibase.fbjavaex.exception.ExceptionTranslator;

import ru.ibase.fbjavaex.managers.*;
import ru.ibase.fbjavaex.jqgrid.*;

/**
 * Конфигурационный класс Spring IoC контейнера
 */
@Configuration
public class JooqConfig {


    /**
     * Возвращает пул коннектов
     *
     * @return 
     */
    @Bean(name = "dataSource")
    public DataSource getDataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        // определяем конфигурацию подключения
        dataSource.setUrl("jdbc:firebirdsql://localhost:3050/examples");
        dataSource.setDriverClassName("org.firebirdsql.jdbc.FBDriver");
        dataSource.setUsername("SYSDBA");
        dataSource.setPassword("masterkey");
        dataSource.setConnectionProperties("charSet=utf-8");
        return dataSource;
    }

    /**
     * Возращает менеджер транзакций
     * 
     * @return 
     */
    @Bean(name = "transactionManager")
    public DataSourceTransactionManager getTransactionManager() {
        return new DataSourceTransactionManager(getDataSource());
    }

    @Bean(name = "transactionAwareDataSource")
    public TransactionAwareDataSourceProxy getTransactionAwareDataSource() {
        return new TransactionAwareDataSourceProxy(getDataSource());
    }

    /**
     * Возвращает провайдер подключений
     * 
     * @return 
     */
    @Bean(name = "connectionProvider")
    public DataSourceConnectionProvider getConnectionProvider() {
        return new DataSourceConnectionProvider(getTransactionAwareDataSource());
    }

    /**
     * Возвращает транслятор исключений
     * 
     * @return 
     */
    @Bean(name = "exceptionTranslator")
    public ExceptionTranslator getExceptionTranslator() {
        return new ExceptionTranslator();
    }

    /**
     * Возвращает конфигурацию DSL контекста
     *
     * @return 
     */
    @Bean(name = "dslConfig")
    public org.jooq.Configuration getDslConfig() {
        DefaultConfiguration config = new DefaultConfiguration();
        // используем диалект SQL СУБД Firebird
        config.setSQLDialect(SQLDialect.FIREBIRD);
        config.setConnectionProvider(getConnectionProvider());
        DefaultExecuteListenerProvider listenerProvider = new DefaultExecuteListenerProvider(getExceptionTranslator());
        config.setExecuteListenerProvider(listenerProvider);
        return config;
    }

    /**
     * Возвращает DSL контекст
     *
     * @return 
     */
    @Bean(name = "dsl")
    public DSLContext getDsl() {
        org.jooq.Configuration config = this.getDslConfig();
        return new DefaultDSLContext(config);
    }

    /**
     * Возвращает менеджер заказчиков
     * 
     * @return 
     */
    @Bean(name = "customerManager")
    public CustomerManager getCustomerManager() {
        return new CustomerManager();
    }

    /**
     * Возвращает грид с заказчиками
     * 
     * @return 
     */
    @Bean(name = "customerGrid")
    public JqGridCustomer getCustomerGrid() {
        return new JqGridCustomer();
    }

    /**
     * Возвращает менеджер продуктов
     * 
     * @return 
     */
    @Bean(name = "productManager")
    public ProductManager getProductManager() {
        return new ProductManager();
    }

    /**
     * Возвращает грид с товарами
     * 
     * @return 
     */
    @Bean(name = "productGrid")
    public JqGridProduct getProductGrid() {
        return new JqGridProduct();
    }

    /**
     * Возвращает менеджер счёт фактур
     * 
     * @return 
     */
    @Bean(name = "invoiceManager")
    public InvoiceManager getInvoiceManager() {
        return new InvoiceManager();
    }

    /**
     * Возвращает грид с заголовками счёт фактур
     * 
     * @return 
     */
    @Bean(name = "invoiceGrid")
    public JqGridInvoice getInvoiceGrid() {
        return new JqGridInvoice();
    }

    /**
     * Возвращает грид с позициями счёт фактуры
     * 
     * @return 
     */
    @Bean(name = "invoiceLineGrid")
    public JqGridInvoiceLine getInvoiceLineGrid() {
        return new JqGridInvoiceLine();
    }

    /**
     * Возвращает рабочий период
     * 
     * @return 
     */
    @Bean(name = "workingPeriod")
    public WorkingPeriod getWorkingPeriod() {
        return new WorkingPeriod();
    }

}



Построение SQL запросов используя jOOQ



Прежде чем рассматривать реализацию менеджеров и сеток (grids) расскажем, как работать с базой данных через jOOQ. Здесь будут изложены лишь краткие сведения о построении запросов, полную документацию по этому вопросу вы можете найти в главе sql-building документации jOOQ.

Класс org.jooq.impl.DSL является основным классом, от которого вы будете создавать все объекты jOOQ. Он выступает в роли статической фабрики для табличных выражений, выражений столбцов (или полей), условных выражений и многих других частей запроса.

DSLContext ссылается на объект org.jooq.Configuration, который настраивает поведение jOOQ, при выполнении запросов. В отличие от статического DSL, DSLContext позволяет создавать SQL-операторы, которые уже «настроены» и готовы к выполнению. В нашем приложении DSLContext создаётся в классе конфигурации JooqConfig в методе getDsl. Конфигурация для DSLContext возвращается методом getDslConfig. В этом методе мы указали, что будем использовать диалект SQL СУБД Firebird, провайдер подключений (определяет, как мы получаем подключение через JDBC) и слушатель выполнения SQL запросов.

jOOQ поставляется с собственным DSL (или Domain Specific Language), который эмулирует SQL в Java. Это означает, что вы можете писать SQL-операторы почти так, как если бы Java изначально поддерживал их, примерно так же, как .NET в C# делает это с помощью LINQ к SQL.
jOOQ использует неформальную BNF нотацию, которая моделирует унифицированный SQL диалект, подходящий для большинства СУБД. В отличие от других, более простых фреймворков, которые используют «Fluent API» или «метод цепочек”, иерархия интерфейса BNF на основе jOOQ не позволяет плохой синтаксис запросов.

Давайте рассмотрим простой запрос на языке SQL
SELECT *
  FROM author a
  JOIN book b ON a.id = b.author_id
 WHERE a.year_of_birth > 1920
   AND a.first_name = 'Paulo'
 ORDER BY b.title


В jOOQ он будет выглядеть следующим образом:
Result result =
dsl.select()
   .from(AUTHOR.as("a"))
   .join(BOOK.as("b")).on(a.ID.equal(b.AUTHOR_ID))
   .where(a.YEAR_OF_BIRTH.greaterThan(1920)
   .and(a.FIRST_NAME.equal("Paulo")))
   .orderBy(b.TITLE)
   .fetch();


Классы AUTHOR и BOOK, описывающие соответствующие таблицы должны быть сгенерированы заранее. Процесс генерации классов jOOQ по заданной схеме БД был описан выше.

В данном случае мы задали таблицам AUTHOR и BOOK алиас с помощью конструкции as. Без использования алиасов этот запрос выглядел бы следующим образом
Result result =
dsl.select()
   .from(AUTHOR)
   .join(BOOK).on(AUTHOR.ID.equal(BOOK.AUTHOR_ID))
   .where(AUTHOR.YEAR_OF_BIRTH.greaterThan(1920)
   .and(AUTHOR.FIRST_NAME.equal("Paulo")))
   .orderBy(BOOK.TITLE)
   .fetch();


Теперь посмотрим более сложный запрос с использованием агрегатных функций и группировки.
SELECT AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, COUNT(*)
FROM AUTHOR
  JOIN BOOK ON AUTHOR.ID = BOOK.AUTHOR_ID
WHERE BOOK.LANGUAGE = 'DE'
  AND BOOK.PUBLISHED > '2008-01-01'
GROUP BY AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME
  HAVING COUNT(*) > 5
ORDER BY AUTHOR.LAST_NAME ASC NULLS FIRST
  OFFSET 1 ROWS
  FETCH FIRST 2 ROWS ONLY


В jOOQ он будет выглядеть так:
dsl.select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, count())
   .from(AUTHOR)
   .join(BOOK).on(BOOK.AUTHOR_ID.equal(AUTHOR.ID))
   .where(BOOK.LANGUAGE.equal("DE"))
   .and(BOOK.PUBLISHED.greaterThan("2008-01-01"))
   .groupBy(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
   .having(count().greaterThan(5))
   .orderBy(AUTHOR.LAST_NAME.asc().nullsFirst())
   .limit(2)
   .offset(1)
   .fetch();


Заметьте ограничение на количество возвращаемых записей, будет сгенерировано в соответствии с указанным диалектом SQL. В примере выше использовался диалект FIREIRD_3_0. Если бы был указан диалект FIREBIRD_2_5 или просто FIREBIRD, то использовалось бы предложение ROWS вместо OFSET … FETCH.

Вы можете собирать запрос по частям. Это позволяет менять его динамически, что можно использовать для изменения порядка сортировки или добавления дополнительных параметров фильтрации.
        SelectFinalStep query = select.getQuery();
        switch (searchOper) {
            case "eq":
                query.addConditions(PRODUCT.NAME.eq(searchString));
                break;
            case "bw":
                query.addConditions(PRODUCT.NAME.startsWith(searchString));
                break;
            case "cn":
                query.addConditions(PRODUCT.NAME.contains(searchString));
                break;
        }
        switch (sOrd) {
            case "asc":
                query.addOrderBy(PRODUCT.NAME.asc());
                break;
            case "desc":
                query.addOrderBy(PRODUCT.NAME.desc());
                break;
        }
        return query.fetchMaps();


Именованные и неименованные параметры



По умолчанию каждый раз, когда вы используете в запросе литера строк, дат и чисел, а также подставляете внешние переменные, jOOQ делает привязку этой переменной или литерала через неименованные параметры. Например, следующее выражение на языке Java
dsl.select()
   .from(BOOK)
   .where(BOOK.ID.equal(5))
   .and(BOOK.TITLE.equal("Animal Farm"))
   .fetch();


Эквивалентно полной форме записи
dsl.select()
   .from(BOOK)
   .where(BOOK.ID.equal(val(5)))
   .and(BOOK.TITLE.equal(val("Animal Farm")))
   .fetch();


и преобразуется в sql запрос
SELECT *
FROM BOOK
WHERE BOOK.ID = ?
  AND BOOK.TITLE = ?


Вам не нужно беспокоиться какой индекс у соответствующего параметра, значения автоматически будут привязаны к нужному параметру. Если нужно изменить значение параметра, то вы можете сделать это, выбрав нужный параметр по номеру индекса (индексация начинается с 1).
Select param = select.getParam("2");
Param.setValue("Animals as Leaders");


Другим способом присвоить параметру новое значение является вызов метода bind.
Query query1 = 
  dsl.select()
     .from(AUTHOR)
     .where(LAST_NAME.equal("Poe"));
query1.bind(1, "Orwell");


Кроме того, jOOQ поддерживает именованные параметры. В этом случае их надо явно создавать, используя org.jooq.Param.
// Create a query with a named parameter. You can then use that name for 
// accessing the parameter again
Query query1 = 
  dsl.select()
     .from(AUTHOR)
     .where(LAST_NAME.equal(param("lastName", "Poe")));
Param query) {
        switch (this.searchOper) {
            case "eq":
                // CUSTOMER.NAME = ?
                query.addConditions(CUSTOMER.NAME.eq(this.searchString));
                break;
            case "bw":
                // CUSTOMER.NAME STARTING WITH ?
                query.addConditions(CUSTOMER.NAME.startsWith(this.searchString));
                break;
            case "cn":
                // CUSTOMER.NAME CONTAINING ?
                query.addConditions(CUSTOMER.NAME.contains(this.searchString));
                break;
        }
    }


    /**
     * Возвращает общее количество записей
     *
     * @return
     */
    @Override
    public int getCountRecord() {
        // запрос, возвращающий количество записей
        SelectFinalStep query = select.getQuery();
        // если мы осуществляем поиск, то добавляем условие поиска
        if (this.searchFlag) {
            makeSearchCondition(query);
        }
        // возарщаем количество
        return (int) query.fetch().getValue(0, 0);
    }

    /**
     * Возвращает записи грида
     * 
     * @return
     */
    @Override
    public List> getRecords() {
        // Базовый запрос на выборку
        SelectFinalStep query = select.getQuery();
        // если мы осуществляем поиск, то добавляем условие поиска
        if (this.searchFlag) {
            makeSearchCondition(query);
        }
        // задаём порядок сортировки
        switch (this.sOrd) {
            case "asc":
                query.addOrderBy(CUSTOMER.NAME.asc());
                break;
            case "desc":
                query.addOrderBy(CUSTOMER.NAME.desc());
                break;
        }
        // ограничиваем количество записей
        if (this.limit != 0) {
            query.addLimit(this.limit);
        }
        // смещение
        if (this.offset != 0) {
            query.addOffset(this.offset);
        }
        // возвращаем массив карт
        return query.fetchMaps();
    }
}



Добавление, редактирование и удаление заказчика мы будем осуществлять через класс CustomerManager, который является своеобразным бизнес слоем между соответствующим контроллером и базой данных. Все операции в этом слое мы будем осуществлять в транзакции с уровнем изолированности Snapshot.
CustomerManager.java
package ru.ibase.fbjavaex.managers;

import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;

import static ru.ibase.fbjavaex.exampledb.Tables.CUSTOMER;
import static ru.ibase.fbjavaex.exampledb.Sequences.GEN_CUSTOMER_ID;

/**
 * Менеджер заказчиков
 *
 * @author Simonov Denis
 */
public class CustomerManager {

    @Autowired(required = true)
    private DSLContext dsl;
    
    /**
     * Добавление заказчика
     * 
     * @param name
     * @param address
     * @param zipcode
     * @param phone 
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)  
    public void create(String name, String address, String zipcode, String phone) {
        if (zipcode != null) {
            if (zipcode.trim().isEmpty()) {
                zipcode = null;
            }
        }        
        
        int customerId = this.dsl.nextval(GEN_CUSTOMER_ID).intValue();
               
        this.dsl
                .insertInto(CUSTOMER,
                        CUSTOMER.CUSTOMER_ID,
                        CUSTOMER.NAME,
                        CUSTOMER.ADDRESS,
                        CUSTOMER.ZIPCODE,
                        CUSTOMER.PHONE)
                .values(
                        customerId,
                        name,
                        address,
                        zipcode,
                        phone
                )
                .execute();
    }

    /**
     * Редактирование заказчика
     * 
     * @param customerId
     * @param name
     * @param address
     * @param zipcode
     * @param phone 
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)      
    public void edit(int customerId, String name, String address, String zipcode, String phone) {

        if (zipcode != null) {
            if (zipcode.trim().isEmpty()) {
                zipcode = null;
            }
        }
               
        this.dsl.update(CUSTOMER)
                .set(CUSTOMER.NAME, name)
                .set(CUSTOMER.ADDRESS, address)
                .set(CUSTOMER.ZIPCODE, zipcode)
                .set(CUSTOMER.PHONE, phone)
                .where(CUSTOMER.CUSTOMER_ID.eq(customerId))
                .execute();
    }

    /**
     * Удаление заказчика
     * 
     * @param customerId 
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)    
    public void delete(int customerId) {
        this.dsl.deleteFrom(CUSTOMER)
                .where(CUSTOMER.CUSTOMER_ID.eq(customerId))
                .execute();
    }
}



Теперь перейдём к написанию контроллера. Классы контроллеров начинаются с аннотации Controller. Для определения действий контроллера необходимо добавить аннотацию @RequestMapping перед методом и указать в ней маршрут, по которому будет вызвано действие контроллера. Маршрут указывается в параметре value. В параметре method можно указать метод HTTP запроса (PUT, GET, POST, DELETE). Входной точкой нашего контроллера будет метод index, он отвечает за отображение JSP страницы (представления). Эта страница содержит разметку для отображения грида, панель инструментов и навигации.

Данные для отображения загружаются асинхронно компонентом jqGrid (маршрут /customer/getdata). С данным маршрутом связан метод getData. Метод содержит дополнительную аннотацию @ResponseBody, которая говорит о том, что наш метод возвращает объект для сериализации в один из форматов. В аннотации @RequestMapping задан параметр produces = MediaType.APPLICATION_JSON, что обозначает, что возвращаемый объект будет сериализован в формат JSON. Именно в этом методе мы работаем с классом JqGridCustomer описанном выше. Аннотация @RequestParam позволяет извлечь значение параметра из HTTP запроса. Данный метод класса работает с GET запросами. Параметр value в аннотации @RequestParam задаёт имя параметра HTTP запроса для извлечения. Параметр required задаёт, является ли параметр HTTP запроса обязательным. Параметр defaultValue задаёт значение по умолчанию, которое будет подставлено в случае отсутствия HHTP параметра.

Метод addCustomer предназначен для добавления нового заказчика. Он связан с маршрутом /customer/create, и в отличие от предыдущего метода работает с POST запросом. Метод возвращает {success: true} в случае успешного добавления, и объект с текстом ошибки в случае ошибки. Данный метод работает с классом бизнес слоя CustomerManager.

Метод editCustomer связан с маршрутом /customer/edit и предназначен для редактирования заказчика. Метод deleteCustomer связан с маршрутом /customer/delete и предназначен для удаления заказчика.

CustomerController.java
package ru.ibase.fbjavaex.controllers;

import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RequestParam;
import javax.ws.rs.core.MediaType;

import org.springframework.beans.factory.annotation.Autowired;

import ru.ibase.fbjavaex.managers.CustomerManager;

import ru.ibase.fbjavaex.jqgrid.JqGridCustomer;
import ru.ibase.fbjavaex.jqgrid.JqGridData;


/**
 * Контроллер заказчиков
 *
 * @author Simonov Denis
 */
@Controller
public class CustomerController {


    @Autowired(required = true)
    private JqGridCustomer customerGrid;
    
    @Autowired(required = true)
    private CustomerManager customerManager;


    /**
     * Действие по умолчанию
     * Возвращает имя JSP страницы (представления) для отображения
     *
     * @param map
     * @return имя JSP шаблона
     */
    @RequestMapping(value = "/customer/", method = RequestMethod.GET)
    public String index(ModelMap map) {
        return "customer";
    }

    /**
     * Возвращает данные в формате JSON для jqGrid
     *
     * @param rows количество строк на страницу
     * @param page номер страницы
     * @param sIdx поле для сортировки
     * @param sOrd порядок сортировки
     * @param search должен ли осуществляться поиск
     * @param searchField поле поиска
     * @param searchString значение поиска
     * @param searchOper операция поиска
     * @return JSON для jqGrid
     */
    @RequestMapping(value = "/customer/getdata", 
            method = RequestMethod.GET, 
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public JqGridData getData(
            // количество записей на странице
            @RequestParam(value = "rows", required = false, defaultValue = "20") int rows,
            // номер текущей страницы
            @RequestParam(value = "page", required = false, defaultValue = "1") int page,
            // поле для сортировки
            @RequestParam(value = "sidx", required = false, defaultValue = "") String sIdx,
            // направление сортировки
            @RequestParam(value = "sord", required = false, defaultValue = "asc") String sOrd,
            // осуществляется ли поиск
            @RequestParam(value = "_search", required = false, defaultValue = "false") Boolean search,
            // поле поиска
            @RequestParam(value = "searchField", required = false, defaultValue = "") String searchField,
            // значение поиска
            @RequestParam(value = "searchString", required = false, defaultValue = "") String searchString,
            // операция поиска
            @RequestParam(value = "searchOper", required = false, defaultValue = "") String searchOper,
            // фильтр
            @RequestParam(value="filters", required=false, defaultValue="") String filters) {
        customerGrid.setLimit(rows);
        customerGrid.setPageNo(page);
        customerGrid.setOrderBy(sIdx, sOrd);
        if (search) {
            customerGrid.setSearchCondition(searchField, searchString, searchOper);
        }

        return customerGrid.getJqGridData();
    }

    @RequestMapping(value = "/customer/create", 
            method = RequestMethod.POST, 
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public Map addCustomer(
            @RequestParam(value = "NAME", required = true, defaultValue = "") String name,
            @RequestParam(value = "ADDRESS", required = false, defaultValue = "") String address,
            @RequestParam(value = "ZIPCODE", required = false, defaultValue = "") String zipcode,
            @RequestParam(value = "PHONE", required = false, defaultValue = "") String phone) {
        Map map = new HashMap<>();
        try {
            customerManager.create(name, address, zipcode, phone);
            map.put("success", true);
        } catch (Exception ex) {
            map.put("error", ex.getMessage());
        }
        return map;
    }

    @RequestMapping(value = "/customer/edit", 
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public Map editCustomer(
            @RequestParam(value = "CUSTOMER_ID", required = true, defaultValue = "0") int customerId,
            @RequestParam(value = "NAME", required = true, defaultValue = "") String name,
            @RequestParam(value = "ADDRESS", required = false, defaultValue = "") String address,
            @RequestParam(value = "ZIPCODE", required = false, defaultValue = "") String zipcode,
            @RequestParam(value = "PHONE", required = false, defaultValue = "") String phone) {
        Map map = new HashMap<>();
        try {
            customerManager.edit(customerId, name, address, zipcode, phone);
            map.put("success", true);
        } catch (Exception ex) {
            map.put("error", ex.getMessage());
        }
        return map;
    }

    @RequestMapping(value = "/customer/delete", 
            method = RequestMethod.POST, 
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public Map deleteCustomer(
            @RequestParam(value = "CUSTOMER_ID", required = true, defaultValue = "0") int customerId) {
        Map map = new HashMap<>();
        try {
            customerManager.delete(customerId);
            map.put("success", true);
        } catch (Exception ex) {
            map.put("error", ex.getMessage());
        }
        return map;
    }
}



JSP страница для отображения справочника заказчиков не содержит ничего особенного: разметку с основными частями страницы, таблицу для отображения грида и блок для отображения панели навигации. JSP шаблоны не очень продвинутое средство, при желании вы можете заменить их на другие системы шаблонов, которые поддерживают наследование. В файле ../jspf/head.jspf содержатся общие скрипты и стили для всех страниц сайта, а файл ../jspf/menu.jspf главное меню сайта. Мы не будем приводить их код, он довольно простой и при желании вы можете посмотреть его в исходных кодах проекта.
Customers.jsp






    
        
        
        
        
        
           
    
    
        
        

        

Customers


© 2016 - Пример Spring MVC приложения с использованием Firebird и jOOQ




Основная логика на стороне клиента сосредоточена в JavaScript модуле /resources/js/jqGridCustomer.js
jqGridCustomer.js
var JqGridCustomer = (function ($) {

    return function (options) {
        var jqGridCustomer = {
            dbGrid: null,
            // опции
            options: $.extend({
                baseAddress: null,
                showEditorPanel: true
            }, options),
            // возвращает модель
            getColModel: function () {
                return [
                    {
                        label: 'Id', // подпись
                        name: 'CUSTOMER_ID', // имя поля
                        key: true, // признак ключевого поля
                        hidden: true          // скрыт 
                    },
                    {
                        label: 'Name', // подпись поля
                        name: 'NAME', // имя поля
                        width: 240, // ширина
                        sortable: true, // разрешена сортировка
                        editable: true, // разрешено редактирование
                        edittype: "text", // тип поля в редакторе
                        search: true, // разрешён поиск
                        searchoptions: {
                            sopt: ['eq', 'bw', 'cn'] // разрешённые операторы поиска
                        },
                        editoptions: {size: 30, maxlength: 60}, // размер и максимальная длина для поля ввода
                        editrules: {required: true}      // говорит о том, что поле обязательное
                    },
                    {
                        label: 'Address',
                        name: 'ADDRESS',
                        width: 300,
                        sortable: false, // запрещаем сортировку
                        editable: true, // редактируемое
                        search: false, // запрещаем поиск
                        edittype: "textarea", // мемо поле
                        editoptions: {maxlength: 250, cols: 30, rows: 4}
                    },
                    {
                        label: 'Zip Code',
                        name: 'ZIPCODE',
                        width: 30,
                        sortable: false,
                        editable: true,
                        search: false,
                        edittype: "text",
                        editoptions: {size: 30, maxlength: 10}
                    },
                    {
                        label: 'Phone',
                        name: 'PHONE',
                        width: 80,
                        sortable: false,
                        editable: true,
                        search: false,
                        edittype: "text",
                        editoptions: {size: 30, maxlength: 14}
                    }
                ];
            },
            // инициализация грида
            initGrid: function () {
                // url для получения данных
                var url = jqGridCustomer.options.baseAddress + '/customer/getdata';
                jqGridCustomer.dbGrid = $("#jqGridCustomer").jqGrid({
                    url: url,
                    datatype: "json", // формат получения данных 
                    mtype: "GET", // тип http запроса
                    colModel: jqGridCustomer.getColModel(),
                    rowNum: 500, // число отображаемых строк
                    loadonce: false, // загрузка только один раз
                    sortname: 'NAME', // сортировка по умолчанию по столбцу NAME
                    sortorder: "asc", // порядок сортировки
                    width: window.innerWidth - 80, // ширина грида
                    height: 500, // высота грида
                    viewrecords: true, // отображать количество записей
                    guiStyle: "bootstrap",
                    iconSet: "fontAwesome",
                    caption: "Customers", // подпись к гриду
                    // элемент для отображения навигации
                    pager: 'jqPagerCustomer'
                });
            },
            // опции редактирования
            getEditOptions: function () {
                return {
                    url: jqGridCustomer.options.baseAddress + '/customer/edit',
                    reloadAfterSubmit: true,
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    drag: true,
                    width: 400,
                    afterSubmit: jqGridCustomer.afterSubmit,
                    editData: {
                        // дополнительно к значениям из формы передаём ключевое поле
                        CUSTOMER_ID: function () {
                            // получаем текущую строку
                            var selectedRow = jqGridCustomer.dbGrid.getGridParam("selrow");
                            // получаем значение интересующего нас поля
                            var value = jqGridCustomer.dbGrid.getCell(selectedRow, 'CUSTOMER_ID');
                            return value;
                        }
                    }
                };
            },
            // опции добавления
            getAddOptions: function () {
                return {
                    url: jqGridCustomer.options.baseAddress + '/customer/create',
                    reloadAfterSubmit: true,
                    closeOnEscape: true,
                    closeAfterAdd: true,
                    drag: true,
                    width: 400,
                    afterSubmit: jqGridCustomer.afterSubmit
                };
            },
            // опции удаления
            getDeleteOptions: function () {
                return {
                    url: jqGridCustomer.options.baseAddress + '/customer/delete',
                    reloadAfterSubmit: true,
                    closeOnEscape: true,
                    closeAfterDelete: true,
                    drag: true,
                    msg: "Удалить выделенного заказчика?",
                    afterSubmit: jqGridCustomer.afterSubmit,
                    delData: {
                        // передаём ключевое поле
                        CUSTOMER_ID: function () {
                            var selectedRow = jqGridCustomer.dbGrid.getGridParam("selrow");
                            var value = jqGridCustomer.dbGrid.getCell(selectedRow, 'CUSTOMER_ID');
                            return value;
                        }
                    }
                };
            },
            // инициализация панели навигации вместе с диалогами редактирования
            initPagerWithEditors: function () {
                jqGridCustomer.dbGrid.jqGrid('navGrid', '#jqPagerCustomer',
                        {
                            // кнопки
                            search: true, // поиск
                            add: true, // добавление
                            edit: true, // редактирование
                            del: true, // удаление
                            view: true, // просмотр записи
                            refresh: true, // обновление
                            // подписи кнопок
                            searchtext: "Поиск",
                            addtext: "Добавить",
                            edittext: "Изменить",
                            deltext: "Удалить",
                            viewtext: "Смотреть",
                            viewtitle: "Выбранная запись",
                            refreshtext: "Обновить"
                        },
                        jqGridCustomer.getEditOptions(),
                        jqGridCustomer.getAddOptions(),
                        jqGridCustomer.getDeleteOptions()
                        );
            },
            // инициализация панели навигации вместе без диалогов редактирования
            initPagerWithoutEditors: function () {
                jqGridCustomer.dbGrid.jqGrid('navGrid', '#jqPagerCustomer',
                        {
                            // кнопки
                            search: true, // поиск
                            add: false, // добавление
                            edit: false, // редактирование
                            del: false, // удаление
                            view: false, // просмотр записи
                            refresh: true, // обновление
                            // подписи кнопок
                            searchtext: "Поиск",
                            viewtext: "Смотреть",
                            viewtitle: "Выбранная запись",
                            refreshtext: "Обновить"
                        }
                );
            },
            // инициализация панели навигации
            initPager: function () {
                if (jqGridCustomer.options.showEditorPanel) {
                    jqGridCustomer.initPagerWithEditors();
                } else {
                    jqGridCustomer.initPagerWithoutEditors();
                }
            },
            // инициализация
            init: function () {
                jqGridCustomer.initGrid();
                jqGridCustomer.initPager();
            },
            // обработчик результатов обработки форм (операций)
            afterSubmit: function (response, postdata) {
                var responseData = response.responseJSON;
                // проверяем результат на наличие сообщений об ошибках
                if (responseData.hasOwnProperty("error")) {
                    if (responseData.error.length) {
                        return [false, responseData.error];
                    }
                } else {
                    // если не была возвращена ошибка обновляем грид
                    $(this).jqGrid(
                            'setGridParam',
                            {
                                datatype: 'json'
                            }
                    ).trigger('reloadGrid');
                }
                return [true, "", 0];
            }
        };
        jqGridCustomer.init();
        return jqGridCustomer;
    };
})(jQuery);



Сетка jqGrid создаётся в методе initGrid и привязывается к html элементу с идентификатором jqGridCustomer. Описание столбцов (колонок) грида возвращается методом getColModel. Каждый столбец в jqGrid имеет достаточно много возможных свойств. В исходном коде присутствуют комментарии, объясняющие свойства столбцов. Подробнее о конфигурировании модели столбцов jqGrid вы можете прочитать в документации проекта jqGrid в разделе ColModel API.

Панель навигации может быть создана с кнопками редактирования или без них (методы initPagerWithEditors и initPagerWithoutEditors соответственно). Конструктор панели прикрепляет её к элементу с идентификатором jqPagerCustomer. Опции создания панели навигации описаны в разделе Navigator документации jqGrid.

Функции getEditOptions, getAddOptions, getDeleteOptions возвращают опции диалогов редактирования, добавления и удаления соответственно. Свойство url указывает, по какому адресу будут отправлены данные после нажатия кнопки OK в диалоге. Свойство afterSubmit – событие, происходящее после отправки данных на сервер и получения ответа от него. В методе afterSubmit проверяется, не вернул ли наш контроллер ошибку. Если ошибки не было, то производится обновление грида, в противном случае ошибка сообщается пользователю. Обратите внимание на свойство editData. Оно позволяет задать значения дополнительных полей, которые не участвуют в диалоге редактирования. Дело в том, что диалоги редактирования не включают в себя значение скрытых полей, а отображать автоматически генерируемые ключи не сильно хочется.

Создание журналов



В отличие от справочников журналы содержат довольно большое количество записей и являются часто пополняемыми. Большинство журналов содержат поле с датой создания документа. Чтобы уменьшить количество выбираемых данных обычно принято вводить такое понятие как рабочий период для того, чтобы уменьшить объём данных передаваемый на клиента. Рабочий период – это диапазон дат, внутри которого требуются рабочие документы. Рабочий период описывается классом WorkingPeriod. Этот класс создаётся через бин workingPeriod в классе конфигурации ru.ibase.fbjavaex.config.JooqConfig.
WorkingPeriod.java
package ru.ibase.fbjavaex.config;

import java.sql.Timestamp;
import java.time.LocalDateTime;

/**
 * Рабочий период
 *
 * @author Simonov Denis
 */
public class WorkingPeriod {

    private Timestamp beginDate;
    private Timestamp endDate;

    /** 
     * Конструктор
     */
    WorkingPeriod() {
        // в реальных приложениях вычисляется от текущей даты
        this.beginDate = Timestamp.valueOf("2015-06-01 00:00:00");
        this.endDate = Timestamp.valueOf(LocalDateTime.now().plusDays(1));
    }

    /**
     * Возвращает дату начала рабочего периода
     *
     * @return
     */
    public Timestamp getBeginDate() {
        return this.beginDate;
    }

    /**
     * Возвращает дату окончания рабочего периода
     * 
     * @return
     */
    public Timestamp getEndDate() {
        return this.endDate;
    }

    /**
     * Установка даты начала рабочего периода
     * 
     * @param value
     */
    public void setBeginDate(Timestamp value) {
        this.beginDate = value;
    }

    /**
     * Установка даты окончания рабочего периода
     * 
     * @param value
     */
    public void setEndDate(Timestamp value) {
        this.endDate = value;
    }

    /**
     * Установка рабочего периода
     * 
     * @param beginDate
     * @param endDate
     */
    public void setRangeDate(Timestamp beginDate, Timestamp endDate) {
        this.beginDate = beginDate;
        this.endDate = endDate;
    }
}



В нашем приложении будет один журнал «Счёт-фактуры». Счёт-фактура – состоит из заголовка, где описываются общие атрибуты (номер, дата, заказчик …), и позиций счёт-фактуры (наименование товара, количество, стоимостью и т.д.). Шапка счёт-фактуру отображается в основной сетке, а позиции могут быть просмотрены в детализирующей сетке, которая раскрывается по щелчку по значку «+» на нужном документе.

Реализуем класс для просмотра шапок счёт-фактуры через jqGrid, он будет наследоваться от нашего абстрактного класса ru.ibase.fbjavaex.jqgrid.JqGrid, описанного выше. В нём имеется возможность поиска наименованию заказчика и дате счёта. Кроме того данный класс поддерживает сортировку по дате в обоих направлениях.
JqGridInvoice.java
package ru.ibase.fbjavaex.jqgrid;

import java.sql.*;
import org.jooq.*;

import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import ru.ibase.fbjavaex.config.WorkingPeriod;

import static ru.ibase.fbjavaex.exampledb.Tables.INVOICE;
import static ru.ibase.fbjavaex.exampledb.Tables.CUSTOMER;

/**
 * Обработчик грида для журнала счёт-фактур
 *
 * @author Simonov Denis
 */
public class JqGridInvoice extends JqGrid {
    
    @Autowired(required = true)
    private WorkingPeriod workingPeriod;    

    /**
     * Добавление условия поиска
     *
     * @param query
     */
    private void makeSearchCondition(SelectQuery select
                = dsl.selectCount()
                        .from(INVOICE)
                        .where(INVOICE.INVOICE_DATE.between(this.workingPeriod.getBeginDate(), this.workingPeriod.getEndDate()));

        SelectQuery select
                = dsl.select(
                        INVOICE.INVOICE_ID,
                        INVOICE.CUSTOMER_ID,
                        CUSTOMER.NAME.as("CUSTOMER_NAME"),
                        INVOICE.INVOICE_DATE,
                        INVOICE.PAID,
                        INVOICE.TOTAL_SALE)
                        .from(INVOICE)
                        .innerJoin(CUSTOMER).on(CUSTOMER.CUSTOMER_ID.eq(INVOICE.CUSTOMER_ID))
                        .where(INVOICE.INVOICE_DATE.between(this.workingPeriod.getBeginDate(), this.workingPeriod.getEndDate()));

        SelectQuery select
                = dsl.selectCount()
                        .from(INVOICE_LINE)
                        .where(INVOICE_LINE.INVOICE_ID.eq(this.invoiceId));

        SelectQuery select
                = dsl.select(
                        INVOICE_LINE.INVOICE_LINE_ID,
                        INVOICE_LINE.INVOICE_ID,
                        INVOICE_LINE.PRODUCT_ID,
                        PRODUCT.NAME.as("PRODUCT_NAME"),
                        INVOICE_LINE.QUANTITY,
                        INVOICE_LINE.SALE_PRICE,
                        INVOICE_LINE.SALE_PRICE.mul(INVOICE_LINE.QUANTITY).as("TOTAL"))
                        .from(INVOICE_LINE)
                        .innerJoin(PRODUCT).on(PRODUCT.PRODUCT_ID.eq(INVOICE_LINE.PRODUCT_ID))
                        .where(INVOICE_LINE.INVOICE_ID.eq(this.invoiceId));

        SelectQuery> httpMessageConverters) {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        jsonConverter.setObjectMapper(objectMapper);
        httpMessageConverters.add(jsonConverter);
    }
…

}


Метод initBinder контроллера InvoiceController описывает, каким образом текстовое представление даты, присылаемое браузером, преобразуется в значение типа Timestamp.

JSP страница содержит разметку для отображения сетки с шапками счёт-фактур и панель навигации. Позиции счёт фактур отображаются при раскрытии счёт шапки фактуры, как выпадающий грид.

Invoices.jsp






    
        
        

        
        
           
           
           
    
    
        
        

        

Invoices


© 2016 - Пример Spring MVC приложения с использованием Firebird и jOOQ




Основная логика на стороне клиента сосредоточена в JavaScript модуле /resources/js/jqGridInvoice.js
jqGridInvoice.js
var JqGridInvoice = (function ($, jqGridProductFactory, jqGridCustomerFactory) {

    return function (options) {
        var jqGridInvoice = {
            dbGrid: null,
            detailGrid: null,
            // опции
            options: $.extend({
                baseAddress: null
            }, options),
            // возвращает опции колонок (модель) счёт фактуры
            getInvoiceColModel: function () {
                return [
                    {
                        label: 'Id', // подпись
                        name: 'INVOICE_ID', // имя поля
                        key: true, // признак ключевого поля   
                        hidden: true // скрыт 
                    },
                    {
                        label: 'Customer Id', // подпись
                        name: 'CUSTOMER_ID', // имя поля
                        hidden: true, // скрыт 
                        editrules: {edithidden: true, required: true}, // скрытое и требуемое
                        editable: true, // редактируемое
                        edittype: 'custom', // собственный тип
                        editoptions: {
                            custom_element: function (value, options) {
                                // добавляем скрытый input
                                return $("")
                                        .attr('type', 'hidden')
                                        .attr('rowid', options.rowId)
                                        .addClass("FormElement")
                                        .addClass("form-control")
                                        .val(value)
                                        .get(0);
                            }
                        }
                    },
                    {
                        label: 'Date',
                        name: 'INVOICE_DATE',
                        width: 60, // ширина
                        sortable: true, // позволять сортировку
                        editable: true, // редактируемое
                        search: true, // разрешён поиск
                        edittype: "text", // тип поля ввода
            
	        
	        
	        

Метки:  

[Перевод] Как заработать 80 000 $ на App Store

Вторник, 27 Июня 2017 г. 09:15 + в цитатник
Это проще, чем вы думаете: не нужно ни удачи, ни упорства.

На конференции WWDC Apple сообщили, что выплатили разработчикам уже 70 миллиардов долларов, причем 30% этой суммы (то есть 21 миллиард!) приходится на прошлый год. Такой резкий скачок меня удивил: не сказал бы, что я и мои друзья стали больше тратить на приложения в последнее время. Но это только мой личный опыт, поэтому я задался вопросом: откуда берется такая выручка? Я открыл App Store и стал просматривать список самых прибыльных приложений.



Шаг первый: На запах денег


Пролистывая список в категории «Производительность», я видел приложения от известных компаний, таких как Dropbox, Evernote, и Microsoft. Ничего удивительного. Стоп, а это что? Десятую позицию в списке самых прибыльных приложений для производительности (рейтинг от 7 июня 2017 года) занимало приложение под названием «Mobile protection :Clean & Security VPN».



Ужасно оформленное название (заглавные буквы вперемешку со строчными, съехавшее двоеточие, корявая грамматика) наводило на мысли, что в поисковом алгоритме случился сбой. Поэтому я отправился на Sensor Tower, чтобы посмотреть, сколько собрало приложение, и увидел… 80 000 $ за месяц?! Теперь мне стало по-настоящему интересно.

Я перешел на страницу приложения и увидел, что как разработчик заявлен Ngan Vo Thi Thuy. Погодите, то есть это VPN сервис от независимого разработчика, который даже не потрудился зарегистрировать компанию? Это серьезный повод для сомнений. Для тех, кто не понимает, что в этом такого, поясню: VPN, по сути, пропускает весь ваш трафик через сторонний сервер. В нашем случае получается, что какой-то непонятный человек, который не в состоянии грамотно сформулировать название и поленился зарегистрировать компанию, хочет получить доступ ко всему вашему Интернет-трафику.

Был и другой тревожный звоночек — тарабарщина в описании:



Здесь утверждается, что в Mobile protection :Clean & Security VPN «куча функций» — да уж, «куча» здесь подходящее слово. Оказывается, «мобильная защита» включает в себя защиту от контактов-«дублликатов». И этот поиск дубликатов, как говорится на скриншотах, состоит из «быстрого и полного сканирования Интернет-безопасность». Даю пять интернетов тому, кто объяснит мне, какая связь между Интернет-безопасностью и дубликатами в контактах.

Масса подозрительных признаков — а ведь я еще даже не скачал приложение. Я перешел на вкладку с отзывами и обнаружил несколько туманных, явно заказных отзывов с оценкой в пять звезд:



Даты этих отзывов натолкнули меня на еще один вопрос: когда было опубликовано приложение? По данным Sensor Tower, Mobile protection :Clean & Security VPN было в числе двадцати самых прибыльных приложений начиная как минимум с 20 апреля 2017 года (то есть уже почти два месяца).

Шаг второй: Подозрительное поведение


Заинтригованный этим якобы золотым приложением, я загрузил его на телефон. Вот что произошло, когда я запустил его в первый раз:



Да-да, «Приложению необходим доступ к Вашим контактам, чтобы осуществить поиск». И все, что я могу сделать, — нажать на кнопку «Agree» («Согласен»), после чего система спрашивает, хочу ли я предоставить приложению «дступ» к контактам. Эээ, нет, спасибо.



Получив отказ, приложение сообщает мне, что мой девайс находится под угрозой. Еще бы! Оно изъявляет готовность «проанализировать девайс», провести Полное или Быстрое Сканирование и обезопасить мне Интернет (жду не дождусь).

По клику на Device Analyze («Проанализировать девайс») отображается информация о том, сколько памяти осталось на айфоне — бесполезная функция, не имеющая никакого отношения к безопасности.

Если же кликнуть хоть на Полное, хоть на Быстрое сканирование появится следующее сообщение:

«Ваши контакты очищены. Дублликатов не найдено».

Прекрасно. Никаких дублликатов, если не считать лишней «л» в слове «дубликаты», конечно.

Ну хорошо, давайте теперь наконец-то обезопасим мне Интернет, нажав на соответствующую кнопку. Ну-ка, ну-ка, что у нас тут?



На экране появляется вот такое неслыханно щедрое предложение сыграть в игру — пострелять по шарикам. Ее даже не нужно устанавливать! Не знаю, за что я удостоился такого замечательного подарка, но придется отложить его до лучших времен. Нажимаю на крестик, чтобы вернуться к своей миссии обезопасить Интернет.

На следующем экране вижу такую картину:



Естественно, я не могу упустить возможность получить «Полный доступ умный анти-вирус прямо сейчас» и нажимаю на «Free trial» («Начать бесплатно»). Бесплатно же!

Touch ID? Без проблем! Стоп… что там написано мелким шрифтом?



«Full Virus, Malware scanner» («Полное сканирование на вирусы, вредоносные программы»). Что? Насколько я знаю, никакое стороннее приложение не может проверить мой айфон на вирусы, так как доступ к файлам у них ограничен песочницей — собственной рабочей директорией. Но давайте дочитаем до конца.

«Недельная подписка будет стоить 99.99 $»

Э… в смысле?

Где-то в недрах стены текста, мелким шрифтом iOS между делом сообщает мне, что одним нажатием на кнопку Home я соглашусь приобрести подписку ценой в 100 $. И это бы еще полбеды, но 100 $ с меня будут брать еженедельно! Один-единственный Touch ID отделял меня от того, чтобы отдать 400 $ за месячную подписку мошеннику, который будет перенаправлять мой трафик?

Что тут скажешь, слава богу, что я прочитал все, что было написано мелким шрифтом. Но все ли будут проявлять такую осмотрительность?

Шаг третий: С миру по нитке


Теперь тот факт, что приложение собирает выручку в 80 000 $ ежемесячно, вдруг стал выглядеть вполне логично. Если каждый подписчик приносит по 400 $ в месяц, нужно обмануть всего 200 человек, чтобы получать 80 000 $ в месяц и, соответсвенно, 960 000 $ в год.

Возможно, вы все еще настроены скептически. Вы, наверное, думаете: «Ну да, 200 человек — это не так много, но как-то не верится, что кто-нибудь вообще станет скачивать такое сомнительное приложение, не говоря уж о том, чтобы за него платить».

Вы-то, может быть, и не станете. Я так точно не стал бы. Но я и на объявление в Google никогда не кликаю, и тем не менее, сервис Adwords как-то принес компании 700 миллиардов долларов на сегодняшний день. В рейтинге приложений App Store по скачиваниям Mobile protection :Clean & Security VPN числится 144-м в категории «Производительность», с порядка 50 000 скачиваний только за апрель.

Чтобы получить 200 подписчиков с 50 000 скачиваний, достаточно уровня конверсии в 0.4% — а то и меньше, потому что подписки продлеваются автоматически, так что клиенты копятся от месяца к месяцу. Разве вы не можете себе представить, чтобы какой-нибудь из ваших не смыслящих в технике родственников случайно (а то и намеренно) оформил «бесплатную» подписку, чтобы защитить свой айпад от вирусов?

Но как этому приложению вообще удалось собрать 50 000 скачиваний?

Я вспомнил, что где-то читал: приложения из App Store находят преимущественно через внутренний поиск по маркету. Возможно, для этого приложения просто выполнили ASO на высшем уровне? Я вбил в поисковую строку на маркете «virus scanner».



Первым в выдаче на правах рекламы стоит Protection for iPhone — Mobile Security VPN. Есть в нем что-то знакомое. Это не клон того, которое мы рассматриваем, но оно содержит встроенную покупку «Free Trial to Premium Protection», которая стоит 99.99 $ и занимает 33-ю позицию среди самых прибыльных в категории «Бизнес».

Как выясняется, мошенники злоупотребляют новым, еще не отшлифованным инструментом от Apple — App Store Search Ads. Они пользуются тем, что рекламируемые приложения не проходят отбор и не модерируются, мало чем отличаются на вид от обычных результатов поиска и в некоторых случаях занимают всю первую страницу поисковой выдачи.

Позже я подробнее изучил эту тему и убедился, что, к сожалению, это не отдельные случаи — подобные приложения регулярно встречаются в топе прибыльных. И они не концентрируются только в выдаче по запросам, связанным с безопасностью. Похоже, мошенники делают ставку на широкий набор ключей. Вот выдача по запросу «wifi»:



На первой же строчке — WEP Password Generator, обычный генератор случайных паролей, который берет с пользователей 50 $ в месяц. Таким образом в месяц он собирает уже по 10 000 $, хотя вышел на рынок только в апреле. По всем признакам, это клон другого приложения, из чего мы можем сделать вывод: данная схема приобрела такую популярность, что мошенники уже начали копировать друг друга.

Чиним App Store: что можете сделать лично вы?


Начнем с того, что, если вы разработчик, не слишком обремененный нормами морали, — примите мои поздравления! Вы только что узнали способ зарабатывать десятки тысяч долларов на App Store без особого труда — по крайней мере, до тех пор, пока они не изменят политику. Если же нет, то могу посоветовать следующее:

  1. Покажите своим родственникам и друзьям, которые хуже разбираются в технике, как просматривать свои подписки и отписываться. Если они стали жертвой такой схемы, помогите им вернуть деньги.
  2. Сообщайте администрации о всех мошеннических приложениях, которые вам встретятся.
  3. Распространяйте информацию о проблеме, пока Apple ей не займется.


Чиним App Store: что должны сделать в Apple?


Как-то с трудом верится, что Apple до сих пор не в курсе проблемы; ведь приложения, о которых шла речь — не мелочь какая-нибудь, они повально занимают высокие позиции в рейтингах. Возможно, компания просто считает ситуацию недостаточно серьезной, чтобы тратить на нее время, либо слишком выгодной для Search Ads или платформы App Store. В любом случае, вот что я бы предложил:

  1. Удалить мошеннические приложения и вернуть пользователям деньги. Это самое очевидное. Просто поставить кому-нибудь задачу регулярно проводить профилактический осмотр топов и вычищать все мошеннические приложения. Как можно заключить из всего сказанного выше, опознать их проще простого. Что касается людей, которые уже попались и купили подписку — автоматически вернуть им деньги за все прошлые покупки в полном объеме.
  2. Изменить интерфейс для покупки подписки через Touch ID. Не использовать мелкий убористый шрифт и не прятать цену где-то в глубине текста (см. скриншот с «Free Trial»). Стоимость должна быть хорошо видна, возможно, стоит ввести задержку в пять секунд, прежде чем пользователь сможет совершить покупку. В качестве бонуса: можно также выводить здесь последние оценки и отзывы на приложение.
  3. Сделать процесс модерации подписок более жестким. Как встроенные покупки за 400 $ с названием «Full Virus, Malware Scanner» вообще проходят модерацию? Есть кто дома? Простой человек, увидев это название в письме с чеком рядом с аккуратной зеленой иконкой, не станет отменять платеж — ведь все выглядит официально, совсем как их чеки из Apple Music. А в некоторых случаях инаппы называются как-нибудь в духе «Free Trial to Premium», хотя никакого бесплатного периода в них не предусмотрено — покупка осуществляется сразу.
  4. Предлагать отменять подписку при удалении приложения. Многие пользователи, которые ставили приложениям-обманкам одну звезду, писали, что даже после удаления приложения с них продолжают взимать плату. В представлении большинства, одно должно следовать из другого — так почему же не следует? При удалении приложения стоит уточнить, не хотят ли пользователи заодно отменить подписку. Конечно, здесь тоже нужно подтверждение, чтобы они ненароком не снесли себе Netflix.
  5. Упростить процедуру отмены подписки. Отказаться от подписки — такое трудоемкое дело, что можно подумать, будто Apple с их фокусом на дизайне нарочно создает для пользователей сложности. На платформе iOS процесс состоит из девяти шагов, ни больше ни меньше. Установить клавиатуру из внешнего источника и то проще (всего шесть шагов). Пожалуйста, уберите лишние трудности. И нет, крохотной кнопки «Сообщить о проблеме» в письме недостаточно. (Update: отменить подписку на одно из мошеннических приложений мне так и не удалось, даже через официальные каналы Apple).
  6. Обезопасить Search Ads. Одна из причин, по которым эту схему так легко реализовать, — внешний вид Search Ads. Многие из опытных пользователей скорее всего даже не подозревают, что кликают на рекламу. Самое меньшее, что может сделать команда Apple — это проверять, не содержит ли рекламная кампания чего-нибудь подозрительного, прежде чем ее запускать (Google и Facebook так и делают), и ясно обозначать, что первые результаты поиска выдаются в качестве рекламы.
  7. Налагать штрафы и подавать в суд. Это предложение я поставил последним в списке, потому что очень маловероятно, что Apple пойдет на что-то подобное. В данный момент нет никакого резона не публиковать мошеннические приложения. В худшем случае ваш аккаунт удалят, но в этом нет ничего страшного — ведь вся выручка сохранится плюс можно спокойно создать новый и начать все сначала. Необходимо оказывать сдерживающее воздействие при помощи штрафов и судебных исков против самых злостных нарушителей.


И в заключение


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

Следовательно, вдобавок к тому, что пользоваться чьей-то неосведомленностью ради собственной выгоды, неэтично, это еще и демотивирует разработчиков. Руки опускаются, когда подумаешь о том, что кто-то добивается финансовых успехов простым и аморальным способом — делая приложения-пустышки, которые можно написать за несколько часов и у которых ровно столько функционала, чтобы красть деньги у тех, кого легко обмануть.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331718/


Метки:  

[Перевод] SecureLogin — забудьте о паролях

Вторник, 27 Июня 2017 г. 09:00 + в цитатник

В начале июня сотрудник компании Sakurity Егор Хомяков (Egor Homakov) написал пост о созданной им технологии SecureLogin, являющейся заменой парольной аутентификации. Несмотря на то что Егор наверняка прекрасно говорит и пишет по-русски, мы не смогли найти русскоязычного варианта и решили сделать перевод оригинальной статьи. Результат вы можете найти под катом.


Сегодня я с гордостью представляю SecureLogin Authentication Protocol 1.0, над которым работал последние 3 года.


Как насчет демки? Легко!


Нет, это не менеджер паролей. Да, это очередная попытка заменить пароли, причем для всех, а не только для гиков.


Кстати, я горжусь не нативными приложениями и реализациями — это была лишь небольшая часть работы, объем которой не превышает нескольких тысяч строк кода.



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


Этот баланс основывается на 3 принципах.


Децентрализация


Никакая третья сторона не должна иметь возможность войти в вашу учетную запись откуда бы то ни было: ни провайдер телефонной связи, сливающий ваши SMS-коды, ни провайдер электронной почты, сбрасывающий ваши пароли, ни Facebook Connect/Google OAuth, выдающий ваш access_token кому-то другому, ни правительства и хакеры, тем или иным способом делающие это через вышеперечисленные сервисы. Только ваше личное устройство должно иметь возможность удостоверять запросы для вашей учетной записи.


На первый взгляд более привлекательные «2FA как сервис»-провайдеры, такие как Authy или Duo, не подпадают под определение end-2-end-децентрализованных, поскольку представляют из себя централизованные службы, подтверждающие запросы от имени пользователя. По большому счету это альтернативная реализация механизма «подтверждение по ссылке в email».


В настоящий момент добиться действительно безопасной аутентификации можно с помощью TOTP (например, Google Authenticator) или USB-ключа типа U2F.


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


Работать с ними очень неудобно. В первом случае приходится записывать резервные коды на бумаге (что я никогда не делал), а второй вариант мало кем поддерживается. Поэтому их уровень проникновения крайне мал.


Пришло время поговорить о втором принципе, на котором основан SecureLogin.


Удобство



Демонстрация пользовательского интерфейса десктопного и мобильного приложений


Это очень похоже на Facebook Connect (исключая зависимость от серверов Facebook): вы нажимаете кнопку Login, приложение открывается, вы подтверждаете запрос, и это все.


Никакой возни с аппаратными ключами, одноразовыми паролями, ожидания электронных писем или SMS, доставания телефона из кармана, сканирования QR-кодов и т. д.


Это настолько просто, насколько вообще возможно для аутентификации.


Масштабирование


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


О резервных копиях беспокоиться не стоит, так как их не существует: ваш закрытый ключ генерируется на основе вашего же мастер-пароля. Серверы SecureLogin не смогут испортить production-базу, так как этой базы не существует. Система работает в автономном режиме (offline).


Никакого аппаратного обеспечения покупать не нужно. Написаны приложения для iOS, Android, macOS, Windows, Linux, и при этом вы всегда можете воспользоваться веб-клиентом.


Протокол полностью свободен (entirely free), и код всех клиентов открыт. За использование системы никогда ничего не придется платить.


API протокола настолько прост, что даже нет необходимости в SDK-библиотеках: 20 строк JS-кода на клиенте, 50 строк на сервере.


Если вы ищете идею для open-source-проекта, рассмотрите реализацию SecureLogin-плагина для вашей любимой CMS. Напишите мне письмо, чтобы присоединиться к нашему Slack.


Кстати, первопроходцы SecureLogin могут получить бесплатный аудит безопасности.


Вопросы?


Буду рад ответить на них в Twitter! Но сначала поищите ответ в FAQ — 90% вопросов повторяют друг друга.


Прошу заметить, что SecureLogin не задумывался как самое безопасное решение, которое покрывало бы все граничные случаи (однако для версии 2.0 планируется функциональность Doublesign), или самое комфортное решение (тут вряд ли удастся переплюнуть Facebook Connect — он слишком удобен). Здесь весь смысл в балансе.


Ссылки:


  1. Оригинал: SecureLogin — Forget About Passwords.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331676/


Метки:  

От Oracle Database 12c EE к PostgreSQL, или основные отличия PostgreSQL при разработке под IEM-платформу Ultimate

Вторник, 27 Июня 2017 г. 08:00 + в цитатник
В предыдущей статье я рассказал о выходе Solid12 — версии IEM-платформы для PostgreSQL. Как и обещал, рассказываем более детально о том, с чем придется столкнуться разработчикам (и с чем столкнулись мы при миграции).
Этот обзор не является исчерпывающим, скорее его нужно рассматривать как минимальную вводную для разработчика под Oracle, приступающего к работе на PostgreSQL.

Итак, вот список отличий, который мы признали наиболее значимыми:
  • Имена таблиц, колонок, хранимых процедур по умолчанию имеют нижний регистр (кроме идентификаторов в кавычках). Это влияет и на имена колонок в результатах SQL-запросов.
  • Пакеты не поддерживаются. Ядро Solid12 использует схемы для группировки функций в пакеты, поэтому используется несколько ядерных схем.
  • Вместо переменных пакетов используются переменные сессии. Семантика их похожа, хотя и имеются некоторые отличия.
  • Временные таблицы в PostgreSQL всегда локальны и не могут быть привязаны к схемам, в отличие от Oracle. Solid12 располагает набором функций, предоставляющий эмуляцию глобальных временных таблиц в стиле Oracle, которые полностью поддерживают стандартный синтаксис для работы с глобальными временными таблицами.
  • Язык хранимых процедур PL/PgSQL похож, но все же отличается от ораклового PL/SQL во многих деталях.
  • Хранимые процедуры всегда используют динамическую привязку (т.е. символы интерпретируются во время выполнения). Например, если хранимая процедура выполняет запрос наподобие «SELECT * FROM USERS», поиск таблицы по имени «USERS» будет запущен во время выполнения, а не при компиляции. Динамическая привязка делает процедуры PostgreSQL весьма гибкими, но подверженными гораздо большему числу ошибок времени выполнения по сравнению с Oracle.
  • По умолчанию, функции PostgreSQL запускаются с набором разрешений текущего пользователя (в Oracle по умолчанию используются разрешения создателя функции). Эту опцию можно явно переопределить в каждой функции, где это требуется.
  • Пустые строки — не то же самое, что NULL. В отличие от Oracle, конкатенация PostgreSQL значений varchar или text с NULL всегда возвращает NULL. Всегда инициализируйте локальные переменные пустыми строчками, чтобы избежать непреднамеренного обнуления результата.
  • Операции DDL в PostgreSQL транзакционны. Создание таблиц и функций, изменение типов колонок, очистку таблиц (TRUNCATE) и т. д. — нужно коммитить.
  • Замена функции новой версией завершится ошибкой, если сигнатура функции отличается от оригинала. В этой ситуации выполнить «CREATE OR REPLACE FUNCTION» недостаточно: потребуется удалить (DROP) старую версию функции и создать ее заново.
  • Любая ошибка базы данных помечает текущую транзакцию как ошибочную, так что транзакция не может выполнять никаких команд, кроме ROLLBACK. Однако такую транзакцию можно откатить к последней именованной точке сохранения (SAVEPOINT), выполненной до возникновения ошибки. После отката к точке сохранения, транзакция может продолжить работу и зафиксироваться (этот прием используется в интеграционных тестах, которые должны продолжать выполнение, невзирая на ошибки).
  • Не поддерживается несколько одновременно активных DataReader-ов на одном соединении с базой данных (так называемый режим MARS — Multiple Active Result Sets). Чтобы прочитать несколько наборов данных, нужно либо открыть несколько соединений к базе данных, по одному на каждый набор данных, либо выполнять запросы по очереди.


От особенностей конкретных баз данных не всегда можно полностью абстрагироваться в прикладном коде. Нередко в командах или службах требуется сформировать и выполнить динамический SQL-запрос, вызвать хранимую процедуру и так далее. В прикладной схеме могут потребоваться триггеры, представления, ограничения или индексы, так что прикладному разработчику Oracle потребуется разобраться хотя бы в базовых свойствах PostgreSQL.
Ниже приводятся кое-какие инструкции, которые могут помочь справиться с некоторыми из описанных трудностей.

Как обойти динамическую привязку в коде PL/PgSQL


Динамическая привязка — это мощный механизм, который в некоторых случаях может заменить динамическое выполнение запросов (EXECUTE sql). Обратной стороной медали является хрупкость конструкции, отсутствие проверок во время компиляции. Компилятор не может статически проверить, ссылается ли данный символ на какой-нибудь объект базы данных.

Когда функция ссылается на символ, например на таблицу или функцию, конкретный объект будет найден по имени только во время выполнения функции. Кроме того, на этот поиск влияет содержимое переменной «search_path», а это означает, что символ может найтись в любой схеме, в зависимости от настроек текущей сессии.
Обычно это задумано не так.

Чтобы отключить динамическую привязку, мы придерживаемся двух простых правил:
  • Добавляем строчку «set search_path to (current schema name)»  ко всем определениям функций и
  • Квалифицируем все таблицы вне текущей схемы именами их схем.


Это не делает привязку статической (PostgreSQL все равно не проверяет валидность символов), но просто отключает возможность непреднамеренной привязки символа к чему-нибудь не тому.
Вот пример исходного кода функции PL/PgSQL, которая больше не страдает от динамической привязки:
-- current search_path = my_schema 
create or replace function my_func(my_arg text) returns void as $$ 
declare v_id bigint; 
begin 
perform another_func(my_arg); -- same as perform my_schema.another_func(my_arg); 
select id into v_id from kernel.users -- table name is qualified with kernel schema name where login = my_arg; -- the rest is skipped... 
end $$ language plpgsql set search_path to my_schema;


Переопределение разрешений, которые применяются к функции


По умолчанию функции PostgreSQL вызываются с набором разрешений текущего пользователя СУБД, подобно опции Oracle «AUTHID CURRENT_USER» (по умолчанию в Oracle действует другой режим — «AUTHID DEFINER»).
Чтобы эмулировать поведение Oracle, функция должна переопределить «security option» вот так:
create or replace function my_secure_func() returns void as $$
begin  -- call here any functions available to the superuser 
end $$ language plpgsql security definer; -- default is security invoker


Эмуляция глобальных временных таблиц в стиле Oracle


Семантика временных таблиц в PostgreSQL существенно отличается от оракловой. Вот краткий обзор отличий:
  • Временные таблицы в Oracle постоянны, то есть их структура фиксированна и видима всем пользователям, а содержимое — временное.
  • В PostgreSQL временная таблица создается перед каждым использованием. И структура, и содержимое временной таблицы видны только текущему процессу СУБД, создавшему эту таблицу. Временные таблицы PostgreSQL всегда удаляются либо в конце сессии, либо в конце транзакции.
  • В Oracle временные таблицы всегда располагаются внутри конкретной схемы, указанной при создании.
  • В PostgreSQL временные таблицы нельзя поместить в произвольную схему, они всегда создаются в специальной неявной временной схеме.


Схема pack_temp содержит библиотеку для эмуляции временных таблиц в стиле Oracle. Нас интересуют всего две функции:
create_permanent_temp_table(table_name [, schema_name]);
drop_permanent_temp_table(table_name [, schema_name]);


Создание постоянной временной таблицы делается в два приема:
  1. Создаем обычную временную таблицу PostgreSQL (ту самую, которая удаляется в конце транзакции).
  2. Вызываем функцию create_permanent_temp_table, чтобы превратить эту временную таблицу в постояную:
    create temporary table if not exists another_temp_table (    first_name varchar,    last_name varchar,    date timestamp(0) with time zone,    primary key(first_name, last_name) ) on commit drop;
    -- create my_schema.another_temp_table select pack_temp.create_permanent_temp_table('another_temp_table', 'my_schema');
    -- or create another_temp_table in the current schema -- select create_permanent_temp_table('another_temp_table');
    -- don't forget to commit: PostgreSQL DDL is transactional commit;
    


При этом создается представление, которое ведет себя точь-в-точь как глобальная временная таблица Oracle. Удалить его можно функцией drop_permanent_temp_table.

Несколько активных DataReader-ов на одном соединении с базой данных


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

Проблема регулярно всплывает в прикладных службах, LINQ-запросах и SQL-запросах во множестве разных форм. Вот несколько типичных случаев:
  1. LINQ-запрос использует константу (или вызывает службу). Запрос открывает первый DataReader, а служба констант пытается открыть второй и получает ошибку. Чтобы избавиться от ошибки, нужно прочитать константу в локальную переменную перед выполнением запроса (или вызывать службу уже после чтения результатов запроса). Пример:
    // было 
    var query = from a in DataContext.GetTable() 
                      where a.ID = Constants.TestAgentID select a;
    // стало 
    var testAgentId = Constants.TestAgentID; 
    var query = from a in DataContext.GetTable() 
                      where a.ID = testAgentId select a;
    
  2. Результаты LINQ-запроса обрабатываются в цикле, но тело цикла выполняет второй запрос, будь то LINQ или SQL. Как обойти ограничение: материализовать результаты запроса в список или массив, и пробежать по списку после того как первый запрос уже выполнен. Пример:
    // было 
    foreach (var langId in DataContext.GetTable().Select(x => x.ID)) 
    { 
      using (LanguageService.UseLanguage(langId)) 
     { 
        // do something language-specific 
     } 
    }
    // стало 
    foreach (var langId in DataContext.GetTable().Select(x => x.ID).ToIDList()) 
    { 
      using (LanguageService.UseLanguage(langId)) 
      { 
          // do something language-specific 
      } 
    }
    

  3. Вызов ToArray/ToList/ToIDList внутри LINQ-зпроса. Чтобы исправить, нужно разбить запрос на части:
    // было 
    var dictionary = DataContext.GetTable().Where(d => dates.Contains(d.DT)) .GroupBy(g => g.DT, e => e.StatusID) .ToDictionary(k => k.Key, e => e.ToIDList());
    // стало 
    var dictionary = DataContext.GetTable() .Where(d => dates.Contains(d.DT)) .GroupBy(g => g.DT, e => e.StatusID) .ToDictionary(p => p.Key);
    var dict = dictionary.ToDictionary(p => p.Key, p => p.Value.ToIDList());
    

    К сожалению, этот вид ошибки очень сложно обнаружить статически. Поэтому каждый нетривиальный LINQ-запрос нужно тщательно протестировать, чтобы убедиться, что он не пытается открыть несколько DataReader-ов одновременно.


Что дальше?


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

Мы не использовали некоторые функции, которые уже есть в PostgreSQL, например поддержку обработки геоданных, но надеемся что сможем использовать их в следующих версиях.

В любом случае, обе версии — light Solid12 и enteprise Ultimate Solid — будут развиваться параллельно, весь важный функционал будет поддерживаться в обоих вариантах поставки платформы.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/329676/


Метки:  

Новые возможности C#, которые можно ожидать в ближайшее время

Вторник, 27 Июня 2017 г. 07:53 + в цитатник


В апреле 2003-его года был выпущен C# 1.2 и с тех пор все версии имели только major версию.
И вот сейчас, если верить официальной страничке roslyn на github, в работе версии 7.1 и 7.2.

Для того, чтобы попробовать версию 7.1 необходимо установить pre-release Visual Studio. Скачать ее можно скачать с официального сайта

Зайдя в свойства решения можно выбрать используемую версию языка.



Далее я хочу рассказать о понравившихся мне новых фичах языка.

Асинхронный Main (планируется в версии 7.1)


В наше время очень часто встречаются программы, которые почти полностью асинхронные.
До сих пор Main мог быть void или int. И мог содержать в себе аргументы в виде массива строк. Сейчас добавятся несколько новых перегрузок:

public static Task Main();
public static Task Main();
public static Task Main(string[] args);
public static Task Main(string[] args);

Так как CLR не поддерживает асинхронные entrypoints, то компилятор сам создает boilerplate code (то есть автоматически вставляет какой-то дополнительный код, который позволяет привести void к Task)
Примерно такой код будет сгенерирован компилятором:

async Task Main(string[] args) {
    // здесь какой-то ваш код
}
// этот метод сгенерируется «за кулисами» автоматически
int $GeneratedMain(string[] args) {
    return Main(args).GetAwaiter().GetResult();
}

Я как и многие ожидал этот функционал еще в 7-ой версии языка.

Литерал default (планируется в версии 7.1)


У Visual Basic и у C# возможности примерно одинаковые. Но в самих языках есть определенные различия.
Скажем у C# есть null, а у VB.NET есть Nothing. Так вот, Nothing может конвертироваться в любой системный тип, представляя собой значение по умолчанию для этого типа. Понятно, что null этого не делает. Хотя, значением по умолчанию вполне может быть и null.

Сейчас уже есть и применяется выражение default(T).
Рассмотрим пример его применения. Допустим, что у нас есть метод, который принимает массив строк в качестве параметра:

void SomeMethod(string[] args)
{
            
}

Можно выполнить метод и передать ему значение массива строк по умолчанию так:

SomeMethod(default(string[]));

Но зачем писать default(string[]), если можно просто написать default?

Вот, начиная с C# 7.1 и можно будет пользоваться литералом default.
Что еще можно будет с ним сделать, кроме как передать значение по умолчанию в метод?
Например, его можно использовать в качестве значения необязательного параметра:

void SomeMethod(string[] args = default)
{
            
}

или инициализировать переменную значением по умолчанию:

int i = default;

А еще можно будет проверить не является ли текущее значение значением по умолчанию:

int i = 1; 
if (i == default) { }   // значением по умолчанию типа int является 0
if (i is default) { }     // точно такая же проверка

Что нельзя будет сделать? Следующие примеры того как литерал default использовать нельзя:

const int? y = default;  
if (default == default)
if (default is T) // оператор is нельзя использовать с default
var i = default
throw default
default as int; // 'as' может быть только reference тип

Readonly ref (планируется в версии 7.2)


При отправке структур в методы в качестве by value (по значению) происходит копирование объекта, что стоит ресурсов. А иногда бывает необходимость отправить только значение ссылочного объекта. В таком случае разработчики отправляют значение по ссылке ref для того, чтобы сэкономить память, но при этом пишут в комментариях что значение изменять нельзя. Особенно часто это происходит при математических операциях с объектами vector и matrix.
Понятный пример того что нас ждет:

    static Vector3 Add (ref readonly Vector3 v1, ref readonly Vector3 v2)
    {
        // так нельзя!
        v1 = default(Vector3);

        // и так нельзя!
        v1.X = 0;

        // так тоже нельзя!
        foo(ref v1.X);

        // а вот теперь нормально
        return new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
    }

Эта функция пока что в состоянии прототипа, но я уверен в ее необходимости.
Одним из предлагаемых вариантов является использование ключевого слова in вместо ref readonly

    static Vector3 Add (in Vector3 v1, in Vector3 v2)
   {
       return new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
   }

Почему in, ведь ref readonly понятнее? А потому что in короче.

Методы интерфейсов по умолчанию (планируется в версии 8.0)


Эти методы мы увидим не так скоро.
Представьте себе, что есть какой-то класс или структура, которые реализуют интерфейс. И вот они смогут унаследовать реализацию одного метода из интерфейса(!) или должны будут реализовать свою версию этого метода.
Уже само понятие реализации в интерфейсе звучит довольно странно.
Хотя у Java 8 есть методы интерфейсов по умолчанию, и разработчики использующие C# тоже захотели себе подобный функционал.
Этот функционал позволит автору API изменить код метода для всех уже существующих реализаций интерфейса.
На примере должно быть понятно.
Допустим у нас есть такой вот интерфейс у которого метод SomeMethod содержит реализацию:

interface IA
{
 void SomeMethod() { WriteLine("Вызван SomeMethod интерфейса IA"); }
}

Теперь создадим класс, которые реализует интерфейс:

class C : IA { }

И создадим экземпляр интерфейса:

IA i = new C();

Теперь мы можем вызвать метод SomeMethod:

i.SomeMethod(); // выведет на экран "Вызван SomeMethod интерфейса IA"

Предположительные имена кортежей (планируется в версии 7.1)


Этот функционал позволит в некоторых случаях не указывать имена кортежей. При определенных условиях элементы кортежа могут получить предположительные имена.

Пример:
Вместо того, чтобы писать (f1: x.f1, f2: x?.f2) можно просто написать (x.f1, x?.f2).
Первый элемент кортежа в таком случчае получит имя f1, а второй f2
Сейчас же кортеж стал бы неименнованным (к элементам которого можно обратиться только по item1, item2 ...)

Предположительные имена особенно удобны при использовании кортежей в LINQ

// c и result содержат в себе элементы с именами f1 и f2
var result = list.Select(c => (c.f1, c.f2)).Where(t => t.f2 == 1);

Отрицательные стороны
Основным недостатком введения этого функционала является совместимость с C# 7.0. Пример:

Action y = () => M();
var t = (x: x, y);
t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда

Но с этим небольшим нарушением совместимости можно смириться, так как период между выходом 7.0 и 7.1 будет небольшим.
Собственно, уже сейчас Visual Studio при использовании 7.0 предупреждает, что
Tuple element name 'y' is inferred. Please use language version 7.1 or greater to access an element by its inferred name.

Полный код примера:

class Program
{
   static void Main(string[] args)
   {
       string x = "demo";
       Action y = () => M();
       var t = (x: x, y);
       t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда  
   }

   private static void M()
   {
       Console.WriteLine("M");
   }
}

public static class MyExtensions
{
   public static void y(this (string s, Action a) tu)
   {
       Console.WriteLine("extension method");
   }
}

Если вы используете .NET 4.6.2 и ниже, то для того чтобы попробовать пример вам необходимо установить NuGet пакет System.ValueTuple
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331554/


Метки:  

Интеграция сценарного тестирования в процесс разработки решений на базе платформы 1С

Вторник, 27 Июня 2017 г. 07:30 + в цитатник
Эта статья является пособием для организации тестирования решений на базе платформы 1С: Предприятие 8.3. Документ отличает практическая направленность, в нем содержится много кода, подходов и умозаключений. Все рассмотренные примеры основаны на использовании бесплатной конфигурации Тестер.


Вступление


Независимо от того, какой философии придерживается компания, одно остается неизменным – процесс программирования приложений с пользовательским интерфейсом, включает в себя запуск программы, эмуляцию действий пользователя, зрительный анализ ситуации, возврат к доработке кода, и так далее, с последующим повторением итерации. Принципиально, в этой схеме можно выделить два отрезка времени: программирование и проверка. Время, которое тратится на программирование, превращается в код. Время, которое тратится на проверки, уходит безвозвратно. В силу специфики процесса разработки, когда мы снова и снова запускаем приложение, повторяя итерации, компенсация потерянного времени достигается за счет уменьшения второго временного отрезка, либо сдвига всех проверок на некую «финальную» итерацию или запуск BDD-теста. Отсутствие возможности повторить опыт проверок, отрицательно сказывается на качестве программного продукта.

Во многом, эти вопросы решаются внедрением специальных методик, например, TDD, BDD, организацией дополнительных процессов по тестированию. Однако, везде есть свои нюансы.

Например, TDD существенно пересматривает взгляд программиста на разработку, и в случае интерактивных приложений с глубокой завязкой бизнес-логики на базе данных, особенно при создании конфигураций с гибкой системой прав и функциональных опций, методику применять не просто.

BDD, за счет человеческого языка сценариев Gherkin, идеологически сшивает в одном месте специалистов прикладной и технической областей, а используемая конструкция описания сценариев Given-When-Then универсальна. Однако, BDD больше о процессе создания продукта, а не только о его тестировании. BDD основывается на ролевом взаимодействии сотрудников компании, что делает её требовательной (и в какой-то мере хрупкой) ко всем звеньям цепи разработки. Другой важной проблемой является не высокая эффективность языка Gherkin при разработке сложных (с точки зрения BDD) сценариев. К автоматическому документированию, как бонусу BDD, также, немало вопросов. Руководства пользователя к продуктам известных вендоров, не строятся по принципу Given – When – Then, а с техническим документированием, многие выбирают подход “Лучше устаревшая, но понятная, чем актуальная, но непонятная (или понятная только автору сценариев) документация.

Исторически, TDD, BDD и другие, пришли из мира, где системы 1С нет как класса, где идет очень четкая специализация, и разрыв между понимаем задачи техническим специалистом и бизнесом, как правило, велик. Например, в типовой 1С-франчайзи, очень часто задачи идут конвейером, могут приниматься по телефону, скайпу или электронной почте. Организация заседаний на каждую доработку в формате трех друзей (Three Amigos), выглядит несерьёзно, а значит почти вся методика BDD искажается или даже ломается.

В тоже время, на крупных проектах, эти переговоры необходимы. В идеале там будет аналитик, тестировщик и разработчик (BA+QA+Developer), но там не будет всего отдела программистов. А им, в конечном итоге, писать код, проводить тестирование, зачастую, намного более глубокое, с пограничными значениями, отклонениями, и другими особенностями реализации, возникновение которых рождается в коде и не может быть описано в приемочном тесте или постановке задачи. И это будет не TDD, а реальные сценарные тесты, которые программисты как правило не автоматизируют, а выполняют вручную в рамках процесса кодирования.

Другой пример из жизни программиста 1С: считается вполне типовым сценарий (пример описан не для российского законодательства): Расчет отпуска сотруднику, где нужно учесть предыдущие начисления за прошлые три месяца, включая количество праздников за период расчета, праздников, выпавших на выходные дни, если они предусмотрены графиком его работы, квартальные, годовые и фиксированной суммой премии, которые могут быть начислены не в периоде их расчетов, смена должности и/или графика работы, учет системы прямого табелирования или методом отклонений, а также, период самого отпуска должен быть очищен от праздников. В случае, если сотрудник часть отпуска проболел – следующее продление должно учитывать предыдущие начисления, кроме этого, нужно предусмотреть пересечение периодов начислений (неделя/две недели/месяц/вахта) с периодом отпуска и другие характеристики. В качестве сценария в данном случае, обычно выступает законодательный акт или статья из бухгалтерской периодики, что в большинстве случаев считается для программиста 1С включенной в область его ответственности задачей. Попытка организовать (а не только написать) сценарное тестирование на языке Gherkin для данного, вполне стандартного сценария, может стать настоящим испытанием.

Несмотря на популярность тех или иных подходов к борьбе за качество программ, для развитых технологий быстрой разработки 1С-приложений, требуется их адаптация. Вопрос не в том, возможно или невозможно применять в отделе программистов ту или иную методику, вопрос в том – насколько это эффективно, и если программисты 1С сами себе начинают писать BDD-сценарии, скорее всего, что-то пошло не так.

Так или иначе, при правильном подходе, любой вид тестирования, как по методике, степени автоматизации и месту применения очень полезен. Например, тестирование, выполняемое тестировщиками, очень важный фильтр вылова ошибок в жизненном цикле разработки программ, и не просто потому что больше – лучше, а потому что специалисты тестирования не предрасположены к совершению ошибок, свойственных разработчикам.

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

О конфигурации Тестер


Бесплатное решение для проведения сценарного тестирования приложений на базе 1С: Предприятие 8.3, управляемые формы. Тестер призван сохранить и воспроизвести опыт программиста, время на приобретение которого было потрачено на ручные проверки и тестирование. Основным профитом от использования Тестера является повышение качества программ, без существенных организационных изменений, изменений принципов программирования, и других долгосрочных инвестиций времени на выпуски очередных версий продуктов. Тестер может использоваться как независимый инструмент, так и совместно с BDD, выступая в качестве платформы для разработки сложных тестов.

Возможности:


  • Программирование и запуск сложных сценарных тестов в одной среде
  • Глубокое тестирование интерфейса и бизнес логики
  • Запись работы пользователя с переводом сценария в программный код
  • Организация коллективной работы по созданию базы тестов
  • Гибкий ролевой доступ, раздельный RLS-доступ пользователей к тестируемым конфигурациям
  • Инкрементальная выгрузка/загрузка тестов во внешние файлы
  • Формирование протоколов и сводных отчетов по выполненным сценариям
  • Настройка рассылки результатов тестов по электронной почте
  • Тестирование по расписанию, организация непрерывного процесса прогона тестов в рамках CI

Особенности:


  • Быстро устанавливается, не требует специальных (кроме 1С) знаний и программного обеспечения
  • Быстро интегрируется в процесс разработки
  • Не требует фундаментального пересмотра философии программирования
  • Сфокусирован на процесс создания реальных тестов
  • Не требует подготовки отдельных баз и эталонных данных

Другое применение:


Тестер может быть использован как автоматизатор рутинных операций, как в процессе разработки, так и в режиме реальной эксплуатации продуктовых баз. Среди таких задач можно выделить:
  • Выгрузка загрузка данных, пакетный запуск 1С для административных задач
  • Запуск и манипуляции обработками, отчетами. Тестером можно написать сценарий, который будет формировать отчет, проверять какие-то данные или открывать обработку и нажимать там нужные кнопки и выбирать поля
  • Формирование начальных или тестовых данных для ваших решений (вместо использования конвертации данных)
  • Нагрузочное тестирование. Например, у вас есть доработка и вы хотите проверить работу этого функционала под нагрузкой. Для этого можно написать сценарий запуска Тестера нужное кол-во раз с передачей целевого тестируемого сценария в качестве параметра
Последние обновления https://github.com/grumagargler/tester
Депо общих тестов https://github.com/grumagargler/CommonTests
Депо демо тестов для ERP2 (демо) https://github.com/grumagargler/ERP2
Сайт проекта http://www.test1c.com
Язык Интерфейс: Английский, Русский
Справка: Английский (частично), Русский

Базовые определения


Процесс тестирования приложений при помощи Тестера основывается на взаимодействии Тестера с запущенной конфигурацией 1С в режиме 1С: Предприятие.

При этом считается, что Тестер выступает в роли Менеджера тестирования, а тестируемая конфигурация – в роли Клиент тестирования.

Тестер является системой сценарного тестирования. Это означает, что взаимодействие с тестируемым приложением происходит в режиме эмуляции действий пользователя.

Всё, что требуется тестировать должно быть описано алгоритмом на языке программирования 1С. Совокупность программируемой логики определяет понятие Сценарий. Все сценарии разрабатываются и хранятся в Тестере.

Тестер может взаимодействовать с тестируемым приложением только так, как это может делать пользователь, от имени которого производится выполнение сценарных тестов. Отсюда формулируется важное замечание: разрабатываемые механизмы тестируемого приложения должны быть проверяемыми. Например, если стоит задача проверить правильность движений документа, и при этом в системе нет отчета по движениям документа и нет отчета/обработки/формы, через которую пользователь смог бы проверить записи, Тестер не сможет проверить правильность движений (в данном случае, таковой является концепция, однако технически, Тестер может запустить внешнюю обработку, в коде которой программист может выполнить произвольный программный код проверки).

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

Тестер не является системой TDD, и строго говоря, не является идеологически чистым BDD. Тестер старается не привязываться к религии в разработке, он решает те проблемы, с которыми сталкиваются абсолютно все разработчики программного обеспечения. Тестер старается решать эти задачи максимально эффективно.

Тестер предназначен для тестирования конфигураций, разработанных на управляемых формах версии 1С: Предприятие 8.3.x. Обычные формы не поддерживаются.

Предполагаемые пользователи Тестера, это:
Пользователи Задачи
Программисты Использование системы в процессе разработки. Эволюция процесса ручного тестирования
Тестировщики с базовыми знаниями программирования на языке 1С Написание сценариев, максимально приближенных к сценариям работы пользователей. Эти сценарии, обычно не такие глубокие, как у программистов, но более выраженные с точки зрения бизнес-процесса
Бизнес аналитики. Консультанты Запуск тестов, анализ результатов. Через чтение тестов, понимание работы функционала системы

Концепция


Источником работы для программиста может служить техническое задание, средства коммуникации, BDD-сценарий, внутреннее вдохновение и многое другое. Вне зависимости от способов формализации процессов в компании, или отсутствию таковых, любое задание адаптируется под внутреннюю специфику работы программистов.

Имея на входе информацию по задаче, мы мысленно разбиваем весь процесс кодирования на небольшие транзакции, рывки. Это является для нас неким краткосрочным планом работ (даже если на входе BDD-сценарий). Затем, мы начинаем методично работать по схеме “написание кода – запуск приложения – проверка ожидаемого поведения – возврат к доработке кода”. Количество таких транзакций и степень их завершенности — очень хрупкие сущности, потому что опираются на способность и возможность программиста быть в сосредоточении.

Даже при идеальных условиях выполнения работ по кодированию, опыт этих транзакций без специальных инструментов – не восстановим. Кроме этого, если в середине намеченного пути, возникает непредусмотренная ситуация, ошибка в связанном коде или недоработка используемого механизма, программисту приходится переключаться в другой контекст для разбора проблемы, нередко делая это в спешке, потому что намеченный план работ начинает ускользать из рабочей памяти нашего мозга. При изменении сопутствующего кода, мы стараемся удержать в голове триггер “нужно протестировать потом этот код”.

Можно использовать специальные метки в коде, инструменты среды разработки, делать скриншоты, аудиозаписи, но это лишь точка возврата к проблеме, ментальный контекст на тот момент времени уже не откатить. Можно применять TDD, но результирующее влияние изменений на поведение системы в целом, проверить будет очень сложно. Например, вы изменили запрос заполнения документа Начисление ЗП, который сильно зависим от условий приема сотрудников на работу, отпусков, больничных, изменений кадровых состояний, и при этом, затрагиваются расчеты налогов, в основе которых лежат эти начисления. Вовлеченность десятков таблиц баз данных, огромного количества начальных значений, может сделать TDD очень сложным, а падающие тесты в таких случаях не всегда понятны из-за отсутствия полного сценарного контекста.

Интегрирование в работу инструмента тестирования, потребует от программиста выработки навыка трансляции мысленно составленного плана работ в программный код сценария. Это дает ему возможность “разгружаться” сериализуясь в код теста. Накапливаемые сценарии, всегда можно воспроизвести, что позволяет сосредоточится на качестве выполняемых работ. Рост количества тестов дает свободу действий не только для рефакторинга, но и других существенных изменениях функциональности разрабатываемого приложения, что является одной из важных концепций инструмента.

Быстрый старт


Для быстрого развертывания инфраструктуры тестирования достаточно выполнить следующие шаги (предполагается, что Тестер скачан и прописан в списке информационных баз):
  1. Конфигурация Тестер должна быть запущена в режиме 1С: Предприятие с ключом /TESTMANAGER.
    Этот параметр может быть прописан непосредственно в профиле информационной базы Тестера, например так:

  2. Тестируемая конфигурация должна быть запущена в режиме 1С: Предприятие с ключом /TESTCLIENT.
    Этот параметр может быть прописан непосредственно в профиле тестируемой информационной базы, например так:
  3. Версия 1С, используемая для тестирования должна быть одинаковой как для Тестера, так и для тестируемой конфигурации.
  4. Запуск Тестера и тестируемой конфигурации желательно производить на одном компьютере. Для запуска программ на разных компьютерах, необходимо настроить порт и адрес тестируемого приложения в справочнике Приложения.
С технической точки зрения, для начала разработки тестов и тестирования больше ничего не требуется.

Первый сценарий


Рассмотрим пример создания теста к конфигурации БСП 2.2.
Напишем элементарный тест, который просто откроет форму списка справочника Партнеры.
  1. Запускаем Тестер с ключом /TESTMANAGER.
  2. Запускаем БСП с ключом /TESTCLIENT.
  3. Переключается в Тестер, открываем меню быстрых функций и создаем новое приложение:

  4. Через меню быстрых функций открываем сценарии и создадим новый сценарий:
    1. В поле ID напишем Test1
    2. В тексте сценария напишем:
    3. Подключить (); // Подключаем БСП к Тестеру
      Меню ( "Справочники / Демо: Партнеры" ); // Открываем в БСП форму списка
    4. Переключимся на вкладку Свойства, и в поле Приложение укажем БСП
    5. Нажмем на панели кнопку Основной
    6. Тест готов. Теперь нажмем кнопку Запустить (или F5 ) и запустим его.
В результате, в БСП должна открыться форма списка справочника Демо: Партнеры. Если бы во время открытия справочника, произошла какая-то ошибка, Тестер бы о ней сообщил.

Второй сценарий


Добавим в первый тест создание нового партнера, для этого внесем следующие изменения в сценарий:
// Подключаем БСП к Тестеру
Подключить ();

// Закроем все окна в БСП
ЗакрытьВсё ();

// Открываем в БСП форму списка
Меню ( "Справочники / Демо: Партнеры" );

// Говорим Тестеру, что мы будем сейчас работать с этим окном
Здесь ( "Демо: Партнеры" );

// Нажмем кнопку Создать
Нажать ( "Создать" );

// Говорим Тестеру, что мы будем сейчас работать с этим окном
Здесь ( "Демо: Партнер (создание)" );

// Установим наименование партнера
Установить ( "Наименование", "Мой тестовый партнер" );

// Кликнем на флажок Поставщик
Нажать ( "Поставщик" );

// Нажмем кнопку Записать и закрыть
Нажать ( "Записать и закрыть" );

После выполнения теста, в базе БСП должен быть новый партнер.
После выполнения теста, в окне сообщений Тестера, будет такое предупреждающее сообщение:
14: Поле "Создать" найдено в нескольких местах: ФормаСоздать (Тестируемая кнопка формы / Кнопка командной панели), СписокКонтекстноеМенюСоздать (Тестируемая кнопка формы / Кнопка командной панели) {Тест1[14]}

Сообщение говорит о том, что в 14 строке кода, метод Нажать нашел несколько мест, где можно нажать Создать.
Для того, чтобы задавать однозначно объекты, с которыми требуется взаимодействие, можно использовать идентификаторы, или полный путь.
Например, в 14 строке можно написать так:
// Вариант 1
Нажать ( "!ФормаСоздать" );

// Вариант 2
Нажать ( "!КоманднаяПанель / Создать" );

Для получения идентификаторов и внутреннего содержимого форм тестируемого приложения, см раздел Вкладка Поля.
Следующие разделы посвящены полному обзору функций Тестера.

Интерфейс


Интерфейс Тестера организован с позиций удобного написания и запуска тестов. При первом запуске, на домашнюю страницу выводится справка по системе (выкачивается из интернета) и открывается основной сценарий, если он задан. В левой части системы находится текстовый редактор для ввода кода сценария, справа – дерево сценариев.
К сожалению, на момент подготовки этой документации, система еще не поддерживает синтаксическое цветовое оформление программных модулей. Лишь поначалу это может показаться серьёзной проблемой, но спустя непродолжительное время, вы перестанете обращать на это внимание, потому что изначально, код сценария существенно проще, чем стандартный программный код конфигураций.
Некоторые специалисты ненадолго предпочли использование Visual Studio Code с расширением Language 1C (BSL) или обычный конфигуратор 1С для набора текста сценария, с последующим копированием или загрузкой в Тестер. Однако, практика показала, что ценность цветового оформления существенно ниже возможности взаимодействия программиста с полями и структурой тестируемого приложения, деревом сценариев, помощником и другими функциями, доступными только из Тестера.

Дерево сценариев


Тестер поставляется с демонстрационной базой. В демо-базе содержится небольшой набор универсальных тестов, а также специальные тесты для конфигурации ERP2. Я предлагаю использовать демо-базу в качестве начальной базы для создания вашей собственной инфраструктуры тестов.
Когда вы запускаете Тестер, в правой части экрана находится дерево сценариев. Это дерево позволяет организовать сценарии в виде иерархии. Каждый узел дерева имеет тип. Тип задает смысловое значение сценариев внутри узла, сортировку и пиктограмму. В терминологии 1С, это дерево – обычный иерархический справочник.
На картинке ниже показан пример дерева тестов из демонстрационной базы, и далее дано описание каждого маркера:


Типы сценариев, Маркер 1


Левее маркера, в зеленом цвете и специальной пиктограмме, находятся такие узлы как Корзина, Общее, Таблица и другие.
Такое оформление означает, что это библиотеки тестов. Признак, что узел является библиотекой, задается при создании/редактировании теста:

На картинке видно, что кроме библиотеки, тест может быть папкой, сценарием или методом.
Строгой технической привязки и контроля типа сценария в Тестере нет. Главное предназначение типов – это организация визуальной логики взаимосвязи сценариев.
В случае, если это библиотека, подразумевается, что внутри данной папки будут храниться только библиотечные сценарии-методы. Метод – это сценарий, который не должен привязываться к конкретной тестируемой логике приложения. Методы обычно могут принимать параметры, они работают как процедуры/функции. Методы не должны запускаться как отдельные, самостоятельные сценарии (запуск сценариев см. здесь).
Пример метода: ПроверитьОшибкуЗаписи.
Пример использования в коде сценария:
Вызвать ( "Общее.ПроверитьОшибкуЗаписи", "Не заполнен контрагент" );

Группы сценариев, Маркер 2


На этом маркере изображена папка Документы. Папка – это логическая группировка сценариев. Группировка может быть произвольной. Вы можете создавать папки с тестами по принципу тестируемых подсистем, ролей пользователей или технических заданий. Если вы не чувствуете, какая структура тестов вам нужна, тогда универсальным подходом является проецирование структуры тестов на объекты метаданных вашей конфигурации. Смело можно создавать группы Справочники, Документы и так далее. Внутри этих групп, создавать группы с названиями объектов метаданных, а следующим уровнем, располагать конкретные сценарии. В будущем, если потребуется, вы сможете сделать перегруппировку.

Обычный сценарий, Маркер 3


Маркером отмечен стандартный сценарий. В данном случае, это сценарий под названием Начало. Обратите внимание, что пиктограмма левее, имеет небольшой желтый шарик справа, а у сценария на маркере 5 такого шарика нет. Наличие желтого шара означает, что внутри данного сценария, кроме скрипта сценария, находится еще и шаблон. Шаблоны используются для проверки бизнес-логики тестируемого приложения. Подробнее о проверке бизнес-логики см. здесь.

Сценарий-метод, Маркер 4


Этим маркером отмечен сценарий-метод. В данном случае, метод находится внутри обычной группы сценариев, а не в библиотеке. В этом случае, предполагается, что метод используется основными сценариями для разгрузки логики. Например, сценарий Начало будет вызывать сценарий-метод ЗадолженностьПоставщикам, который в свою очередь будет формировать отчет и проверять правильность показателей. Тестер, в дереве, располагает сценарии выше методов, чтобы они не смешивались.

Основной сценарий, Маркер 5


Этим маркером отмечен обычный сценарий. Подчеркивание снизу указывает на то, что данный сценарий в настоящий момент текущий (основной). Основной сценарий, это тот сценарий, над которым сейчас работает программист. Любой сценарий может быть установлен основным (правый клик в дереве / Основной). У каждого пользователя системы Тестер может быть свой основной сценарий. Основной сценарий отличается от остальных тем, что его легко найти в дереве (правый клик в дереве / Найти основной сценарий) и легко запустить на выполнение (см. Запуск тестов). Также, основной сценарий автоматически открывается при запуске новой сессии Тестера.

Приложения, Маркер 6


Под маркером 6 находится колонка с названием приложения, к которому принадлежит соответствующий сценарий.
Приложение устанавливается в форме редактирования сценария, для любого типа сценария:

Поле Приложение можно не устанавливать, оставить пустым. В этом случае, предполагается что сценарий универсальный и может работать для всех приложений в системе.
Обратите внимание, что для библиотечных тестов и групп первого уровня (см. картинку с деревом тестов) приложение не задано, а для группы ЗаказПоставщику, ТестСоздания и т.д. приложение задано. Логика следующая: библиотечные тесты могут работать для любой конфигурации, поэтому при их создании, приложение не задали. Группа тестов Документы тоже будет в любой конфигурации, приложение также не задано. Для группы ЗаказПоставщику, ТестСоздания и т.д. приложение задано, потому что они имеет четкую привязку к тестируемому приложению.
Задавать приложение для сценариев нужно:
  1. Для логической группировки сценариев и возможности отбора сценариев в дереве тестов, см. картинку:
  2. Для контроля уникальности имен сценариев. Например, может существовать несколько тестов с названием ЗаказПоставщику, если для каждого из них задано отдельное приложение. Создать еще одну группу тестов с названием Документы нельзя, потому что она задана как общая, без указания конкретного приложения.

Хранилище, Маркер 7


Правее маркера, находится колонка, определяющая в виде пиктограммы статус сценариев в хранилище тестов.
Стратегия редактирования сценариев в Тестере организована по принципу работы со стандартным хранилищем 1С. Для того, чтобы начать редактирование сценария, его нужно захватить (правый клик в дереве / Захватить). Для того, чтобы сценарий сохранить в хранилище тестов, его нужно туда поместить (правый клик в дереве / Поместить). В момент помещения теста в хранилище (или создания нового теста), создается его версия. В тот период времени, пока сценарий захвачен на редактирование, остальные участники тестирования могут использовать захваченный сценарий, но они не могут его изменять. При использовании захваченного сценария, программисты буду получать от Тестера последнюю версию сценария, а не текущую, которая редактируется в настоящий момент.
Например, если вы захватили и редактируете сценарий-метод ЗадолженностьПоставщикам, а другой программист запустил на выполнение сценарий Начало, который в свою очередь из кода вызовет заблокированный вами метод ЗадолженностьПоставщикам, Тестер отдаст этому программисту последнюю версию теста ЗадолженностьПоставщикам, которая была в хранилище до того, как вы начали его редактирование. Таким образом, ваши текущие изменения сценария не повлияют на работу других разработчиков.
При помещении изменений в хранилище, ваши изменения становятся доступны всем пользователям. Если вы разрабатываете определенный функционал, и ваши изменения теста могут затронуть работоспособность других связанных тестов, рекомендуется использовать метод МояВерсия ().

Запуск тестов


Любой тест из дерева тестов может быть запущен на выполнение, вне зависимости от его типа. Однако, как было сказано выше, предполагается, что запускаться должны только тесты с типом Сценарий, а сценарии-методы, должны вызываться из кода сценариев.
Основной способ запуска тестов это кнопка F5 или команда Запустить:

При запуске теста по кнопке F5 (или командe Запустить) Тестер всегда запускает сценарий, установленный как основной. Таким образом, вне зависимости от кого, какой сценарий вы в данный момент редактируете, запускаться будет только основной.
Такой подход позволяет редактировать группу взаимосвязанных тестов, и быстро запускать весь сценарий на выполнение, без ненужных переключений вкладок. Кроме запуска основного сценария, имеется возможность запуска текущего сценария. Полный набор функций см. в контекстных меню Тестера.

Вкладка Поля


В процессе написания сценария, может возникать необходимость в анализе внутренней структуры окон тестируемого приложения.
Такой анализ нужен для выделения идентификаторов полей и точного к ним обращения из кода теста. Также, такой анализ может единственным способом разобраться в составе полей тестируемой формы в случаях, когда поля формируются динамически.
Для такой задачи на форме сценария присутствует вкладка Поля (для новых сценариев, вкладка скрыта, сценарий должен существовать):

По порядку следования маркеров:
  1. Позволяет получить структуру всех окон тестируемого приложения, которые сейчас открыты на экране.
  2. Позволяет получить только текущее окно тестируемого приложения.
  3. Позволяет быстро найти в дереве элементов текущий активный элемент тестируемого приложения. Очень удобная функция при написании тестов. Например, можно открыть нужную форму и встать на нужный элемент, затем, в Тестере, получить эту форму (п.2) и нажать Синхронизировать. После этого, Тестер попытается найти и активировать строку в дереве с данным элементом. В случае неудачи, генерируется ошибка.
  4. При навигации по дереву, Тестер пытается активировать выделенные элементы в тестируемом приложении. Будьте внимательны, фокус может “прыгать” с дерева на тестируемое приложение.
  5. Для выделенного поля можно выполнить метод или получить свойство. Набор методов и свойств зависит от типа выделенного элемента и применяется согласно объектной модели тестируемого приложения платформы 1С. Например, на картинке поле ОтборСостояние имеет тип ТестируемоеПолеФормы. В синтаксис помощнике 1С, можно посмотреть, какие методы и свойства доступны объектам этого типа. Одно из них, свойство ТекстЗаголовка, результат получения которого выведен на картинке выше.

Перед внедрением


Несмотря на простоту использования Тестера, внедрение тестирования требует определенных усилий. На практике, в среднем, нужно от недели до двух для того, чтобы адаптироваться к разработке с Тестером.
Перед погружением в детали, определимся с тем, когда автоматизированное тестирование применять неэффективно:
  1. Вы создаете макет, прототип приложения для демонстрации заказчику
  2. Вы вносите точечные исправления в старый код, в те механизмы, сценариев к которым нет и не будет, и вся эта работа без перспектив эволюции модулей
  3. Вы прощупываете будущую реализацию, когда еще нет четкого понимания, что это должно быть, документ или справочник, отчет или динамический список. В этом случае, программист нередко переключается в режим “потока” и быстро накидывает объекты метаданных для того, чтобы доказать или опровергнуть самому себе достижимость результата выбранным путем. В таких ситуациях, практически невозможно выработать сценарий, но это и не нужно. Строго говоря, пока не сформируется полное представление о способе выполнения задачи, сценарное тестирование применять не стоит
Процесс внедрения нужно начинать плавно. Вначале это может быть один программист или аналитик и небольшой проект, затем, когда в коллективе уже кто-то будет владеть данной техникой, и сможет оперативно отвечать на вопросы, можно постепенно расширять область действия Тестера.

Установка


Если вы занимаетесь обслуживанием клиентов частным образом, Тестер имеет смысл установить локально на ваш компьютер или ноутбук, файловый вариант.
Если вы работаете в команде, вне зависимости от того, работают все программисты над одним проектом или у каждого свой, Тестер желательно установить в локальной сети или облаке. Если ваш коллектив более 3 программистов, я бы рекомендовал установить клиент-серверный вариант платформы.
Даже если ваша команда сильно распределена, с пингом до ~125мс всё еще можно комфортно работать через интернет, разработка Тестера велась с учетом удаленности программистов. Если по каким-то причинам, облачное решение организовать не получается, можно установить программу локально на каждый компьютер специалиста, файловый вариант. Для организации общего хранилища тестов, можно использовать Git. У Тестера есть возможность инкрементальной выгрузки/загрузки тестов в файловую систему.
Кроме этого, для организации ночного тестирования, потребуется установить на выделенном для этого сервере или виртуальной машине тонкий клиент 1С: Предприятие, с возможностью его подключения к облаку, где ведется разработка тестов. В случае использования Git для хранения тестов, нужно будет обеспечить предварительную закачку тестов на сервер тестирования, с использованием утилит от Git. Подробнее, ночное тестирование описано разделе Запуск тестов по расписанию.
Тестер позволяет в одной базе вести работу с тестами для неограниченного числа приложений (конфигураций). Не стоит создавать отдельную базу с Тестером под каждый проект/конфигурацию/клиента. В Тестере возможна настройка ограничения доступа пользователей к приложениям.

Базы данных


Тестирование при помощи Тестера предполагает наличие двух баз данных: рабочей и начальной.
Рабочая база — это база данных в которой работает программист, и в которой выполняются все тесты. Соответственно, у каждого программиста своя рабочая база.
Начальная база — это база, заполненная начальными данными. Начальная база нужна для того, чтобы периодически создавать/перегружать рабочие базы. Наполненность начальной базы зависит от типа выполняемых работ. Начальная база общая для всех программистов.

Создание и обновление начальной базы


Если вы разрабатываете типовую конфигурацию, за основу начальной базы можно взять начальную базу, которую вы будете поставлять с вашей конфигурацией. В этой базе (подготавливаемой для тестирования, а не поставки в решении), рекомендуется включить все функциональные опции, добавить несколько пользователей с разными ролями, заполнить стандартную нормативно-справочную информацию, создать как минимум одну организацию, склад, подразделение и другое, в зависимости от специфики вашего решения.
Если вы дорабатываете типовую конфигурацию, в качестве начальной базы может выступать база клиента, или демонстрационная база типового решения, обновленная и настроенная спецификой вашего заказчика.
Кроме самого факта существования начальных данных, я рекомендую разработать и поддерживать тест, который будет проверять начальную базу на согласованность данных. Например, вам для работы тестов требуется определенный курс валюты, или наличие графика работ Пятидневка, или должен быть заполнен регистр адресации бизнес-процессов типовыми ролями и исполнителями и т.д. С ростом функционала вашей системы, и возможным, в этой связи, расширением начальных данных – доработка теста проверки начальной базы будет всегда гарантировать понимание базовых условий.
Использовать такой тест удобно в следующих случаях:
  • Каждый программист/аналитик/тестировщик может четко проверить консистентность своей рабочей базы перед тем как вообще что-то тестировать (напомню, рабочая база создается на основании начальной)
  • Данный тест можно запускать первым в списке тестов для ночного тестирования, по результатам которого, определить целесообразность дальнейшего тестирования
  • Тест хорошо работает как напоминание разработчику, какие данные можно брать не думая об их существовании, а какие нужно создавать дополнительно. В процессе работы, рабочая база превращается в хлам, и помнить, какие данные там начальные, а какие нет, сложно. Разворачивать каждый раз начальную базу последней версии – может быть не всегда удобно или возможно, при этом, быстрое определение происхождения данных при написании сценария очень важный момент. Например, в базе есть группа материалов “Детали насосно-компрессорного оборудования”, и если есть тест проверки начальных данных, в нем можно произвести поиск по коду и вычислить, происхождение этой группы, буквально в несколько кликов.
Примечание: в качестве решения проблемы загрязнения рабочей базы я не рассматриваю откат транзакций в сценарном цикле, что вы можете найти как подход в некоторых авторитетных источниках. По моему опыту, используя такой подход говорить о сколь-нибудь серьёзном тестировании не приходится.
Начальную базу нужно подключить к хранилищу, куда сливаются финальные обновления конфигурации. Это необходимо для оперативного обновления начальной базы новым функционалом, обновления и/или заполнения базы новыми начальными данными.
Если над решением трудится коллектив, начальную базу нужно расположить в общедоступном месте, чтобы каждый специалист мог в любой момент "обнулить" свою рабочую базу, загрузив в неё начальную.
Если новый функционал требует обновления/добавления начальных данных, такую задачу можно решить двумя способами:
  1. Ответственный разработчик, открывает обновленную начальную базу, дополняет вручную нужными данными, дорабатывает тест проверки начальных данных, запускает этот тест, убеждается в согласованности данных, и выгружает полученную базу в общедоступное место
  2. Всё тоже самое что в п.1 с той разницей, что вместо ручного обновления, ответственный разработчик, пишет сценарный тест.
Второй вариант дольше, но предпочтительней, потому что позволит всем участникам команды обновлять свои рабочие базы без обнуления начальной выгрузкой данных.
Начальные данные желательно использовать в режиме “только чтение" по отношению к тестируемому функционалу. Например, если у вас в начальных данных определена организация по умолчанию, и у вас есть тест, который проверяет механизм ввода новой/изменения/удаления организации, то лучше в этом тесте не использовать в качестве тестовой организации, существующую организацию в начальной базе. Даже если вашим тестом предусмотрен откат изменений к начальному состоянию, нет гарантии, на каком этапе упадет ваш тест. Когда это произойдет, он оставит после себя испорченные начальные данные, что может привести к цепной реакции падения тестов, выполняемых ночью, по расписанию.
Если вашим тестам нужно изменять начальные данные, тогда в начале таких тестов проверяйте, и при необходимости, устанавливайте их начальное состояние. После отработки сценария, верните эти настройки в исходное положение. Если такой тест упадет, тогда другой тест, зависимый от этих же настроек, восстановит начальные значения в исходное состояние перед своим стартом.
Например, если ваш тест проверяет работу механизма контроля остатков в зависимости от системной настройки приложения, вашему тесту придётся изменять эту настройку. Если ваш тест упадет, настройка не будет возвращена в первоначальное положение и следующий запуск этого теста уже вряд ли отработает правильно, даже если с функциональной точки зрения всё в порядке. Таким образом, вначале желательно тестом сходить в настройки, установить требуемое значение и затем продолжить основной сценарий. К слову, подобные проверки не всегда требуются в оперативной работе, поэтому их запуск имеет смысл делать по условию, например, по имени пользователя ночного тестировщика, или флагу глобальных настроек (см. API Тестера).

Эталонная база


Эталонная база данных используется для тестирования бизнес-логики приложения.
Использование эталонной базы при интеграции тестирования в процесс разработки не эффективно и не используется Тестером по следующим причинам:
  1. Требуется особый уход за эталонными данными, их корректировка в соответствии с изменениями функционала решения
  2. Эталонные данные инертны: новый функционал требует новых эталонных данных, которые могут пересекаться с уже существующими эталонными данными. Вытекающая отсюда осторожность при манипуляции с эталонными данными ограничивает или замедляет развитие спектра тестируемого функционала
  3. Оперативный прогон тестов практически невозможен, выгружать/обновлять/загружать эталонные базы долго. Под оперативностью понимается тестирование при каждом запуске программистом приложения на выполнение, а не увеличение числа релизов и закачки схемы на CI-сервере с прогоном тестов в течение дня
  4. Количество эталонных баз стремится расти, это осложняет их обслуживание и весь процесс тестирования
Вместо хранения эталонных данных в эталонной базе, проверяемую бизнес-логику нужно хранить в самом Тестере.
С точки зрения Тестера, проверка бизнес-логики – это совокупность программного кода сценария, в котором проверяются значения тестируемых полей, и результирующая отчетность по данным информационной системы. Подробнее о тестировании бизнес-логики см. здесь.

Данные для тестирования


Данные для тестирования это входящая для сценариев информация, требуемая для успешного их прохождения. Если ваш тест открывает форму списка справочника изделий и устанавливает фильтр по заводу-производителю, вашими тестовыми данными как минимум будут: запись в справочнике заводов-изготовителей, запись в справочнике изделий, с заполненным реквизитом завода-изготовителя. Тестовыми данными в этом случае будут и пользователь, под которым была запущена информационная база, единица измерения для изделия и другие сущности.
Тестовые данные состоят из двух слоёв:
  1. Начальные данные, которые хранятся в начальной базе и обеспечивают минимальный набор типовой, редко изменяемой информации
  2. Создаваемые данные, которые необходимы для тестирования конкретного функционала
Наибольший интерес представляет второй слой. Существует много подходов создания тестовых данных, но в их совокупности можно выделить две стратегии:
  1. Выгрузка заготовленных данных из предварительно подготовленных баз или шаблонов, и последующая их загрузка перед началом или во время тестирования. Вариантов выгрузки/загрузки в данном подходе много. Это может быть конвертация данных, универсальный обмен данными, макеты с данными, выгрузки в dt-файлы или обычная сериализация прикладных объектов в JSON или XML.
    Рассмотрим плюсы и минусы такого подхода:
    Плюсы Минусы
    Очевидность подхода Сложность поддержки согласованности данных в развивающемся функционале приложения, проблемы загрузки данных при измененной структуре метаданных, смене владельцев, появлению полей обязательных для заполнения, переименованию или удалению объектов и их реквизитов.
    Быстрый старт в подготовке данных Потенциальная опасность получения не консистентных данных и ложного положительного прохождения теста. Такие тестовые данные как правило загружаются в специальном режиме, без дополнительных проверок и срабатывания обработчиков форм. Фактически, тестовые данные готовятся не так, как это бы сделал пользователь.
    Сложность логистики хранения, взаимосвязанности наборов файлов, данных и тестов.
    Статичность. Данные загружаются как есть и их сложно менять под вариативность тестов в случае необходимости.
    Проблемы с удалением тестовых данных для повторных запусков сценариев.
  2. Формирование тестовых данных самим сценарием. При этом подходе, все тестовые данные должны создаваться, а не храниться для загрузки. Сам процесс создания не должен отличаться от процесса разработки целевых сценарных тестов. Другими словами, сегодня вы пишите тест проверки создания нового товара, а завтра этот тест уже может быть использован как метод для создания тестовых данных, для тестирования создания документа Реализация товаров.
    Плюс и минусы такого подхода:
    Плюсы Минусы
    Гибкость создания требуемого окружения тестовых данных под различные вариации тестов. Возможность разработки сложных взаимосвязанных тестов. Сложно без опыта выстроить структуру библиотечных тестов для создания тестовых данных
    Высокое качество тестов за счет дополнительного тестирование всего стека используемых объектов при подготовке данных. Это очень важный нюанс, потому что создание тестовых данных таким образом, выполняется для разных видов целевых сценариев, что автоматически покрывает тестами разнообразие условий создания объектов.
    Например, у нас может быть тест-метод создания поставщика. Но поставщик может создаваться из разных контекстов: из формы списка контрагентов или из поля ввода на форме документа. И если тестовые данные загружать “извне”, можно не отловить ошибку создания контрагента из документа. Тест проверки документа пройдет успешно, хотя фактически, пользователь документ ввести не сможет (потому что не сможет создать для документа поставщика).
    Требуется первоначальная инвестиция времени в разработку тестов-методов
    Консистентность полученного тестового окружения, заполненность реквизитов, отработка возможных событий и программных оповещений об изменении данных, другими словами — полная штатная эмуляция работы приложения.
    Слабая связанность с изменчивостью структуры метаданных. Если изменяется структура данных объекта, достаточно изменить (если необходимо) один тест-метод, при этом все остальные тестовые данные менять не придётся.
    Простота хранения. Все тест-методы хранятся в одной среде, и не являются внешними по отношению к системе тестирования.
    Использование одних и тех же тестовых данных даже в случае, когда их версии метаданных отличаются. В этом случае, в тест-методах можно организовывать условия по обработке/заполнению полей в зависимости от версии используемой программистом конфигурации (см. функцию МояВерсия ())
Тестер исповедует вторую стратегию в подготовке тестовых данных. С точки зрения Тестера, формирование тестовых данных не должно быть в отрыве от целевого тестирования, тестовые данные должны быть подвижны, и они не должны готовиться отдельно от системы разработки тестов.

Структура сценария


Перед написанием тестов, нужно задуматься и определить сценарии, которые будут соответствовать вашему краткосрочному планированию. Важно не отвлекаться в этот момент и мысленно не накидывать возможные отклонения. Для каждого отклонения, можно будет потом определить приоритетность и разработать отдельный тест. Даже если вы в состоянии представить большой сценарий, рекомендуется его разделить на несколько небольших и взаимосвязанных тестов.
Для успешного внедрения тестирования, очень важно, чтобы скорость создания и прогона тестов была высокой.
Чтобы быстро создавать тесты, нужны две составляющих: тренировка мышления, для быстрого выбора шаблона сценария под задачу и библиотека готовых тестов-методов, для создания всего необходимого по ходу тестирования. Эти вещи приходят со временем.
Чтобы быстро прогонять тесты, нужно чтобы они были следующей структуры:
  1. Определение окружения
  2. Создание окружения
  3. Целевая часть
Определение окружения – это функция в модуле вашего сценария, которая задает параметры теста в виде структуры. Примеры параметров теста: список приходуемых товаров, с ценами и количеством, наименование поставщика, валюта договора и т.д.
Создание окружения – процедура в модуле теста, которая по переданным параметрам создаст необходимое тесту окружение. В этой же процедуре, должна быть проверка уже созданного ранее окружения, чтобы оно не создавалось при повторном запуске сценария.
Целевая часть – это непосредственно код, который будет проверять то, что вы запланировали сделать, и ничего более.
Скорость прогона тестов достигается за счет выполнения тестом только целевой его части, то есть фактически той, которая выполняется вручную при каждом запуске приложения после модификации программистом. Конечно, создание окружения тоже займет какое-то время, и как минимум один раз, тест должен выполнить эту часть сценария, но и это ровно то, что программист бы сделал вручную. Технически, создание окружения вручную может быть быстрее, но лишь на короткий отрезок жизни приложения.
Яснее на примере. Давайте представим, что вы занимаетесь доработкой документа Списание ТМЗ, который нужно дополнить особыми движениями по регистрам. Для тестирования (неважно как, вручную или нет) вам понадобится материал на складе, себестоимость которого вы знаете, установленные параметры учетной политики, на которые вы полагаетесь, и другие настройки системы, например, вариант контроля остатков. Таким образом, вам нужно будет вручную, как минимум один раз проверить, что настройки системы в порядке. Затем, нужно будет создать несколько материалов, возможно отдельный склад и как минимум одно поступление ТМЗ, и вы не забудете дату поступления сделать днем ранее. Таким образом – вы определяете и создаете окружение, чтобы затем выполнить целевую часть – провести списание и проверить, что особые движения появились и они правильные. Другими словами – выполняются все три описанных выше структурных действия для разработки автоматизированного сценария.
Такой несложный подход не просто перекладывает ручные действия в код, он позволяет вам существенно продвинуться в вопросах развития нового функционала, и тестирования отклонений.
Пример. Добавим в описанную задачу необходимость проверки сообщения об ошибке при списании количества материала, большего, чем есть на складе. Я сильно не ошибусь, если предскажу работу смекалки программиста: он просто откроет уже проведенное списание (предварительно добавив его в избранное для максимальной “скорости”), отменит его проведение, в колонку с количеством введет 999999, запишет документ, а потом проведет его и проанализирует сообщение об ошибке. Если сообщения не будет, программист вернется в код, сделает необходимые доработки и будет повторять итерацию до тех пор, пока сообщение об ошибке не появится.
И тут начинаются проблемы. Во-первых, проверить нормальное поведение списания уже так просто не получится. Нужно будет привести данные в порядок, убрать 999999 и вернуть то количество, которое было на момент, когда мы проверяли правильность доработанных движений. И нам уже вряд удастся быстро вспомнить, каким именно было то первоначальное количество. Во-вторых, даже если программист предварительно скопирует документ, перед тем как менять данные – максимум он потом сможет проверить отсутствие ошибок проведения, но расчетные показатели, результат проведения, он проверить уже не сможет.
Другими словами, имеем классическую картину: последующая доработка механизма потенциальна опасна для предыдущего функционала. И при отсутствии возможности быстрого восстановления окружения теста и его оперативного прогона (а не ночью), потенциальная опасность, рано или поздно перейдет в кинетическую, реальную ошибку.
Дело не только в том, когда мы обнаружим проблему, сейчас, после ночного тестирования, или тестирования тестировщиками, а в том, что когда мы находимся глубоко в коде, нередко нужно здесь и сейчас проверить механизмы, чтобы определиться с выбранным направлением, понять, что наращиваемый функционал не конфликтует и не искажает другие алгоритмы.
Я хотел бы обратить внимание, что даже при условии, когда тесты программиста проходят, они могут содержать методологические ошибки или ошибки понимания задачи. Такие ошибки может выловить отдельный департамент, специальные тестировщики или в конце концов заказчик. Но это отнюдь не означает, что вся история с тестированием программистами не имеет смысла. Наоборот, на основе готовых тестов, программисту достаточно будет скорректировать целевую часть (третья часть сценария), а всю остальную работу по подготовке данные и их проверке выполнит система.

Практика


Перейдем к практической части: поработаем с Тестером в режиме создания нового функционала для конфигурации УТ11 на базе постановки задачи от бизнес-аналитика.

Постановка задачи


Компания торгует сигарами. Необходимо доработать документ Реализация товаров и услуг (далее Реализация) для возможности указания даты согласования следующих продаж по выбранным позициям. Предполагается следующий сценарий:
— Менеджер вводит документ реализации, для выбранных позиций, на своё усмотрение он устанавливает дату, когда ему нужно было бы перезвонить клиенту и уточнить, насколько хорошо продаются сигары. Например, из всего перечня, его интересуют только сигары со вкусом ментола.
— У менеджера есть список, который он открывает каждый день и видит, когда, по каким клиентам и каким позициям ему нужно провести переговоры с клиентом для организации следующих поставок интересующих его позиций.
— Менеджер выбирает нужную позицию из списка, звонит клиенту, и вводит в систему результат переговоров

Документ Реализация

Необходимо в документ Реализация, в табличную часть, добавить поле для ввода даты согласования (без времени).
Поле не обязательно для заполнения. Если поле задано – тогда данная позиция считается отмеченной для согласования. По таким позициям необходимо предусмотреть обособленный учет на базе регистра сведений Согласование. Записи в данный регистр производить при проведении документа. При повторном проведении документа, существующие записи регистра сведений не обновлять, только добавлять новые.

Регистр сведений Согласование

Это новый, независимый регистр сведений, с возможностью редактирования.
Регистр должен хранить такие данные:
  1. Документ Реализация, Клиент, Менеджер, Товар, Количество, Сумма – автоматически должны заполняться при проведении реализации и не должны быть доступны для редактирования
  2. Флаг Отработано – будет устанавливаться пользователем
  3. Флаг Комментарий – будет вводиться пользователем и фиксировать результат переговоров. Поле должно быть доступно для редактирования только если флаг Отработано = истина.
Для регистра необходимо разработать форму списка и форму элемента. В форме списка должны быть предусмотрены отборы по менеджеру, клиенту и товару.
Запретить пользователю вводить новые записи в данный регистр интерактивно. Примечание: запрет на ввод нового сделать программно с выводом соответствующего сообщения. Не использовать вместо этого настройку команд и ролей пользователя.
Объект расположить в подсистеме Продажи / Оптовые продажи.
Тесты

Необходимо разработать следующий тест:
  1. Ввод одной реализации с двумя позициями, одна из которых будет отмечена для согласования
  2. Открытие списка на согласование, отбор по клиенту, открытие формы регистра на редактирование, установка галочки, сохранение и закрытие
  3. Убедиться, что позиция ушла из списка
Примечание: данный тест требуется автором технического задания. Исполнитель (программист) волен разработать столько дополнительных тестов, сколько ему потребуется для обеспечения надлежащего качества выполнения работ.

Разработка


По этой задаче мы можем прикинуть ход разработки и тесты, которые нам понадобятся. Условимся, что начальная база у нас уже есть, в ней проведены нужные настройки, отключены окна-подсказки, проверки контрагентов и так далее (о начальной базе см. здесь). В моем случае, начальной базой будет демо-база УТ11.
Можно выделить как минимум четыре этапа:
Этап 1: добавить реквизит Дата согласования в табличную часть, в процедуре ОбработкаПроверкиЗаполнения реализовать проверку, чтобы дата согласования была больше даты документа
Этап 2: доработать процедуру ОбработкаПроведения, добавить туда логику формирования записей по регистру сведений
Этап 3: реализовать форму списка с фильтрами, как требует бизнес-аналитик
Этап 4: реализовать форму редактирования записи регистра для ввода данных по согласованным позициям.

Этап 1


В таблице ниже состыкуем работу по кодированию и тестированию:
Программирование Тестирование
В Конфигураторе:
  1. Добавляем реквизит ДатаСогласования в табличную часть Товары документа Реализация
  2. В процедуре ОбработкаПроверкиЗаполнения реализуем проверку даты
В Тестере :
Создадим тест ПроверкаДатыСогласования, где:
  1. Введем новую реализацию, добавим строку, впишем в строку некорректную дату
  2. Попытаемся провести, и в списке сообщений найдем требуемое сообщение об ошибке
  3. Затем изменим дату на правильную, проведем и убедимся, что сообщения об ошибке уже нет
Так как это будет наш первый тест для конфигурации УТ11, нам нужно произвести разовую настройку приложения в Тестере.
Необходимо создать приложение УТ11, см. Меню функций / Приложения. Для создаваемого приложения нужно задать актуальную на момент написания теста версию, например так:

Затем, это приложение нужно установить приложением по умолчанию, для этого нужно переключиться в дерево сценариев, открыть Опции, выбрать в поле Приложение УТ11 и нажать кнопку справа:

Следующий шаг – это создание папки РеализацияТоваровУслуг внутри папки Документы, и следом, создание нашего теста ПроверкаДатыСогласования.
В итоге, должно получится так:

Откроем выделенный на картинке сценарий, переключимся в текстовый редактор и введем этот текст:
// Сценарий:
// - Вводим новый документ Реализация
// - Устанавливаем неправильную дату согласования, проверяем наличие ошибки
// - Устанавливаем правильную дату, проверяем отсутствие ошибки

Подключить ();
ЗакрытьВсё ();

// Вводим новый документ
Коммандос ( "e1cib/data/Document.РеализацияТоваровУслуг" );
Здесь ( "Реализация *" );

// Определяем даты
дата = Дата ( Взять ( "!Дата" ) );
плохая = Формат ( дата - 86400, "DLF=D" );
хорошая = Формат ( дата + 86400, "DLF=D" );

// Вводим плохую дату
Нажать ( "!ТоварыДобавить" );
Установить ( "!ТоварыДатаСогласования", плохая );

// Проводим документ
Нажать ( "!ФормаПровести" );
если ( НайтиСообщения ( "Дата согласования*" ).Количество () = 0 ) тогда
	Стоп ( "Должна быть ошибка неверной установки даты согласования" );
конецесли;

// Вводим хорошую дату
товары = Получить ( "!Товары" );
Установить ( "!ТоварыДатаСогласования [1]", хорошая, товары );

// Проводим документ
Нажать ( "!ФормаПровести" );
если ( НайтиСообщения ( "Дата согласования*" ).Количество () > 0 ) тогда
	Стоп ( "Не должно быть ошибок неверной установки даты согласования" );
конецесли
	        
	        
	        

Метки:  

Перевод книги Appium Essentials. Глава 3

Вторник, 27 Июня 2017 г. 07:18 + в цитатник
Глава 3. Appium GUI.

В этой главе речь пойдет о GUI для Appium-сервера. К концу главы мы должны разобраться во всех возможностях приложения.
Содержание
  • Appium GUI для Windows
  • Appium GUI для Mac

А тут у нас ссылки на
главу 1
— и главу 2
Поехали!



Appium GUI для Windows


Разработчики Appium хорошо спроектировали GUI для сервера. Используя этот GUI, мы можем легко запустить сервер со всеми необходимыми нам настройками [Desired capabilities из главы 1]. GUI позволяет настроить окружение перед запуском тестов.

Вот так выглядит Appium GUI. У него есть следующие кнопки/иконки:
  • Настройки Android
  • Общие настройки
  • Настройки разработчика
  • Инфо
  • Инспектор
  • Запуск/Стоп
  • Очистить [логи]



Настройки Android


Нажав на Настройки Android, мы увидим разные опции, которые нам могут понадобиться перед запуском тестов. Все поля распределены по секциям Application (Приложение), Launch Device (Запуск устройства), Capabilities (Возможности) и Advanced (Расширенные):


Application


Список полей в разделе с пояснениями:
Поле Описание
Application Path Здесь указывается путь до файла .apk, который хотите протестировать.
Package Указывает пакет для запуска. Например, com.android.calculator2.
Wait for Package Эта возможность ожидает запуска пакета, который указан в поле Package.
Launch Activity Можно указать активити, которую вы хотите запустить в приложении. Например, MainActivity.
Use Browser Позволяет выбрать из списка браузер для запуска
Full Reset Удаление приложения в конце сессии
No Reset Предотвращает сброс устройства.
Intent Action Используется для запуска активити
Intent Category Категория интента
Intent Flags Выставляет флаги старта активити. [Про флаги можно почитать здесь.]
Intent Arguments Здесь можно задать дополнительные аргументы при старте активити


Launch Device


Поле Описание
Launch AVD Здесь указывается имя AVD для запуска
Device Ready Timeout Таймаут (в секундах) ожидания готовности устройства
Arguments Аргументы запуска эмулятора


Capabilities


Поле Описание
Platform Name Задает имя платформы, на которой будет запущено приложение
Automation Name Имя инструмента автоматизации (можно выбрать из списка)
Platform Version Здесь указывается версия Android, на которой будет тестироваться приложение.
Device Name Имя девайса.
Language Язык, который будет задан на устройстве Android.
Locale Локаль, которая будет задана на Android.


Advanced


Поле Описание
SDK Path Путь к Android SDK.
Coverage Class Здесь задается класс инструментов [подробнее позже].
Bootstrap Port Порт, на котором будет «висеть» Appium.
Selendroid Port Порт для Selendroid.
Chromedriver Port Порт для ChromeDriver [если нужен].


General Settings


Кликните на вторую иконку, чтобы открыть общие настройки. Здесь поля так же разбиты по категориям: Server и Logging:


Server


Поле Описание
Server Address IP-адрес, где запущен Appium-сервер.
Port Порт, по которому Appium-сервер будет передавать команды. По дефолту: 4723.
Check for Updates Если выбрать, Appium будет автоматически проверять наличие обновлений.
Pre-Launch Application Позволяет запустить приложение на девайсе до того, как начнет слушать команды от WebDriver.
Override Existing Session Если активно, текущие сессии будут пересозданы, если они есть.
Use Remote Server Если Appium-сервер запущен на другой машине, вы можете использовать эту опцию, чтобы задействовать Appium Inspector.
Selenium Grid Configuration File Вы можете задать путь до конфиг файла для Selenium Grid.


Logging


Поле Описание
Quiet Logging Задает уровень логирования.
Show Timestamps Вывод в консоль будет сопровождаться датой-временем записи.
Log to File Выведенный лог будет сохранен в указанном файле (например, C:\\appium\\abc.log).
Log to WebHook Лог будет отправлен по HTTP слушателю.
Use Local Timezone Если выберете эту опцию, будет использоваться местная тайм-зону, иначе будет использоваться тайм-зона node-сервера.


Developer settings



Поле Описание
Enabled Настройки разработчика будут доступны, если выбран чек-бокс.
Use External NodeJS Binary Если у вас другая версия Node.js, отличная от установленного с Appium, то можно использовать ее. Нужно задать путь.
Use External Appium Package Здесь можно задать пакет Appium, если у вас есть свой [все-таки open source].
NodeJS Debug Port Порт, на котором будет запущен дебаггер Node.js.
Break on Application Start Как только приложение на девайсе запуститься, дебаггер Node.js остановится.
Custom Server Flags Здесь можно передать серверу флаги для запуска (например, --device-name Nexus 5). [Я так понимаю, речь идет об этих флагах]


About


[Очевидно, здесь пояснений много не требуется. Их и не было. Тут можно посмотреть версию Appium]


Inspector


Appium Inspector позволяет определять на странице элементы. Также доступна возможность записывать и воспроизводить действия, как в Selenium IDE, но текущая версия, работает не очень хорошо на Windows. Чтобы открыть Inspector, нужно нажать на иконку, но, сперва, нужно будет запустить приложение на девайсе:

Используя Inspector, мы можем посмотреть разметку приложения, но все равно сложно определять элементы. На Windows, UIAutomator намного мощнее Appium Inspector по части определения элементов. В следующей главе мы узнаем больше о UIAutomator.

The Launch/Stop button


Запускает/останавливает Appium-сервер

The Clear button


Находится в правом нижнем углу; удаляет логи из консоли.


The Appium GUI for Mac


Для Mac у Appium похожий GUI автоматизации Android; множество опций такое же, как на Windows. Давайте рассмотрим все настройки в GUI.

Он содержит следующие иконки:
  • Android Settings
  • iOS Settings
  • General Settings
  • Developer Settings
  • Robot Settings
  • Save configuration
  • Open configuration
  • Inspector
  • Appium doctor
  • Launch/Stop
  • Delete


Android Settings


Мы уже посмотрели настройки для Android на Windows; на Mac OS настройки такие же, но есть некоторые UI-отличия, скриншот ниже. Настройки Android поделены на две вкладки: Basic и Advanced.


iOS Settings


Чтобы настроить iOS, нам нужно кликнуть на иконку iOS. Она содержит две вкладки: Basic и Advanced. В Basic вкладке расположены подразделы Application и Device Settings, а в Advanced — расширенные iOS настройки.


Application


Поле Описание
App Path Здесь задается путь до iOS-приложения (.app, .zip, or .ipa), которое мы хотим протестировать.
BundleID Задает ID бандла.
Use Mobile Safari Если тестируется мобильное веб-приложение, то можно выбрать эту опцию, чтобы запустить Safari. Убедитесь, что BundleID и App Path не выбраны.


Device Settings


Поле Описание
Force Device Можете выбрать iPhone или iPad симулятор.
Platform Version Используется для выбора версии платформы.
Force Orientation Указывается ориентация экрана на симуляторе.
Force Language Задается язык на симуляторе.
Force Calendar Формат календаря на симуляторе.
Force Locale Локаль симулятора.
UDID Если чекбокс UDID выбран, Appium запустит приложение на подключенном устройстве iOS; нужно убедиться, что bundleID поставлен, а App Path не выбран.
Full Reset Удаляет всю папку симулятора.
No Reset Указывает, на то. что симулятор не должен перезапускаться между сессиями.
Show Simulator Log Логи симулятора будут записываться в консоль.


Advanced
Поле Описание
Use Native Instruments Library Если выбрано, Appium будет отдавать предпочтение нативной библиотеке инструментов.
Backend Retries Мы можем определить количество попыток запуска инструментов перед тем, как репортить крэш или таймаут.
Instruments Launch Timeout В миллисекундах определяет, сколько инструменты должны ждать запуска.
Trace Template Path Файл шаблона, который будет использоваться инструментами.
Xcode Path Путь до XCode.


Robot Settings
Если хотите задействовать в автоматизации робота [пока не очень ясно, какого именно], можете задать настройки в Robot Settings. В разделе Robot Settings, Appium спрашивает хост и порт, к которым подключен робот:


Save/Open configuration
Appium поддерживает возможность сохранения настроек; нам не придется снова и снова определять настройки при тестировании. Опция позволяет вам пометить файл тегом, а потом легко его найти [теги MacOS, видимо].

Чтобы сохранить файл настроек, нужно:
  1. Кликнуть на кнопку Save
  2. Задать имя файла
  3. Кликнуть на Tags текстбокс; отобразится список тегов. Мы можем выбрать больше одного
  4. Выберите место, куда хотите сохранить файл


Сохраненную конфигурацию легко восстановить из файла, нажав кнопку Open.

Appium doctor
Подскажет про настройку Appium; вы можете проверить настройку, используя Appium doctor. Нажмите на иконку Doctor; в консоли отобразится информация.
Если что-то не настроено или не установлено, появится запись. попробуйте устранить проблему перед запуском Appium.

Inspector
Инспектор позволяет нам без труда сгенерировать скрипт автотеста. Он показывает все элементы приложения, как UIAutomator на Android. Теперь давайте рассмотрим Inspector подробнее. Для примера, возьмем калькулятор BMI — приложение на iOS. Сначала, нужно кликнуть на кнопку Save и мы должны задать путь до приложения.

В окне Appium Inspector можно увидеть следующие поля:


  • Показывать неактивные — показывает неактивные элементы.
  • Показывать невидимые — показывает скрытые элементы
  • Запись — откроет панель записи и вы сможете выполнять действия предоставленные средствами Appium Inspector
  • Обновить — обновит DOM в 3-х колонках в соответствии с просмотрщиком
  • Скриншот — в этой области отображается текущий скриншот приложения. Вы можете кликнуть на элемент на скрине, чтобы выбрать его в DOM
  • Детали — подробности о выбранном элементе


Вы найдете еще несколько вкладок, таких как Touch, Text, Locator и Misc, которые определяют действия с приложением.

Коротко о них:
Поле Описание
Touch Содержит кнопки для имитации действий: тап, свайп, скрол и шейк [трясем девайсом].
Text Здесь работа с текстом: ввод текста и выполнение JavaScript.
Locator Полезная опция от Appium. Используя ее, мы можем проверить, что наш локатор возвращает элемент.
Misc Кнопки для обработки разных предупреждений.


Панель записи
Панель содержит тестовые скрипты, сгенерированные рекордером, на основе совершенных действий. Также содержит специфичных для рекордера опций:
Поле Описание
Выбор языка Можно выбрать ЯП, на котором хотим получить тестовый скрипт (на скриншоте выбран Java).
Добавлять Boilerplate [нечто непереводимое] Если выбрано, скрипт будет содержать код, отвечающий за поднятие инстанса Selenium. Если нет — то только сами записанные шаги
Воспроизвести Воспроизводит записанный скрипт.
Misc Кнопки для обработки разных предупреждений.


В следующей главе мы рассмотрим способы идентификации элементов на странице мобильного приложения.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331714/


Метки:  

Регистрация с помощью telegram бота

Вторник, 27 Июня 2017 г. 01:00 + в цитатник
Сейчас почти на всех сайтах есть регистрация. Реализована она чаще всего с помощью e-mail, реже с помощью смс. А что если сделать регистрацию через telegram бота? В качестве логина на сайте мы сможем использовать подтверждённый номер телефона, а сам бот будет посылать одноразовые коды для входа. В данной статье описан процесс создания такого бота на языке Golang.

image
Пример работы бота

Хотелось бы сразу отметить: в статье не будет описана реализация входа на сайт по одноразовым кодам.

Весь код есть в репозитории на GitHub

Оглавление:


  1. Необходимое ПО
  2. Получение API ключа
  3. Структура проекта
  4. Файл настроек
  5. Инициализация бота и соединения с БД
  6. Основной код
  7. Запуск бота

Для начала нужно установить всё необходимое ПО:

Далее следует получить API ключ для нашего будущего бота. Для этого нужно написать BotFather. Что примерно должно получиться:
image

Перед тем как начать программировать нужно определится со структурой проекта, у меня она получилась такой:
/project/
	/conf/
		settings.go
	/src/
		database.go
		telegramBot.go
	main.go

Приступаем к написанию кода!
Для начала файл настроек (settings.go):
const (
	TELEGRAM_BOT_API_KEY = "paste your key here"  // API  ключ, который мы получили у BotFather
	MONGODB_CONNECTION_URL = "localhost"  // Адрес сервера MongoDB
	MONGODB_DATABASE_NAME = "regbot"  // Название базы данных
	MONGODB_COLLECTION_USERS = "users"  // Название таблицы
)

Для каждого пользователя в БД хранятся: ид чата(chat_id) и номер мобильного телефона(phone_number). Поэтому сразу создаём структуру User:
type User struct {
	Chat_ID      int64
	Phone_Number string
}

Соединение с MongoDB реализуем с помощью структуры DatabaseConnection:
type DatabaseConnection struct {
	Session *mgo.Session  // Соединение с сервером
	DB      *mgo.Database // Соединение с базой данных
}

Бота представим в качестве структуры TelegramBot:
type TelegramBot struct {
	API                   *tgbotapi.BotAPI        // API телеграмма
	Updates               tgbotapi.UpdatesChannel // Канал обновлений
	ActiveContactRequests []int64                 // ID чатов, от которых мы ожидаем номер
}

Функция инициализации соединения с MongoDB:
func (connection *DatabaseConnection) Init() {
	session, err := mgo.Dial(conf.MONGODB_CONNECTION_URL)  // Подключение к серверу
	if err != nil {
		log.Fatal(err)  // При ошибке прерываем выполнение программы
	}
	connection.Session = session
	db := session.DB(conf.MONGODB_DATABASE_NAME)  // Подключение к базе данных
	connection.DB = db
}

Функция инициализации бота:
func (telegramBot *TelegramBot) Init() {
	botAPI, err := tgbotapi.NewBotAPI(conf.TELEGRAM_BOT_API_KEY)  // Инициализация API
	if err != nil {
		log.Fatal(err)
	}
	telegramBot.API = botAPI
	botUpdate := tgbotapi.NewUpdate(0)  // Инициализация канала обновлений
	botUpdate.Timeout = 64
	botUpdates, err := telegramBot.API.GetUpdatesChan(botUpdate)
	if err != nil {
		log.Fatal(err)
	}
	telegramBot.Updates = botUpdates
}

Бот инициализируется, но делать он ещё ничего не умеет. Давайте двигаться дальше!
Следующий шаг — «основной цикл бота»:
func (telegramBot *TelegramBot) Start() {
	for update := range telegramBot.Updates {
		if update.Message != nil {
			// Если сообщение есть  -> начинаем обработку
			telegramBot.analyzeUpdate(update)
		}
	}
}

Это бесконечный цикл. Обработка всех входящих сообщений начинается с него. В начале обработки мы должны проверить, есть ли пользователь у нас в базе. Нет — создаём и запрашиваем его номер, есть — продолжаем обработку.
// Начало обработки сообщения
func (telegramBot *TelegramBot) analyzeUpdate(update tgbotapi.Update) {
	chatID := update.Message.Chat.ID
	if telegramBot.findUser(chatID) {  // Есть ли пользователь в БД?
		telegramBot.analyzeUser(update)
	} else {
		telegramBot.createUser(User{chatID, ""})  // Создаём пользователя
		telegramBot.requestContact(chatID)  // Запрашиваем номер
	}
}

func (telegramBot *TelegramBot) findUser(chatID int64) bool {
	find, err := Connection.Find(chatID)
	if err != nil {
		msg := tgbotapi.NewMessage(chatID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
	}
	return find
}

func (telegramBot *TelegramBot) createUser(user User) {
	err := Connection.CreateUser(user)
	if err != nil {
		msg := tgbotapi.NewMessage(user.Chat_ID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
	}
}

func (telegramBot *TelegramBot) requestContact(chatID int64) {
        // Создаём сообщение
	requestContactMessage := tgbotapi.NewMessage(chatID, "Согласны ли вы предоставить ваш номер телефона для регистрации в системе?")
        // Создаём кнопку отправки контакта
	acceptButton := tgbotapi.NewKeyboardButtonContact("Да") 
	declineButton := tgbotapi.NewKeyboardButton("Нет")
        // Создаём клавиатуру
	requestContactReplyKeyboard := tgbotapi.NewReplyKeyboard([]tgbotapi.KeyboardButton{acceptButton, declineButton})
	requestContactMessage.ReplyMarkup = requestContactReplyKeyboard
	telegramBot.API.Send(requestContactMessage)  // Отправляем сообщение
	telegramBot.addContactRequestID(chatID)  // Добавляем ChatID в лист ожидания
}

func (telegramBot *TelegramBot) addContactRequestID(chatID int64) {
	telegramBot.ActiveContactRequests = append(telegramBot.ActiveContactRequests, chatID)
}

Начальная обработка написана, теперь давайте напишем общение с базой данных:
var Connection DatabaseConnection  // Переменная, через которую бот будет обращаться к БД

// Проверка на существование пользователя
func (connection *DatabaseConnection) Find(chatID int64) (bool, error) {
	collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)  // Получаем коллекцию "users"
	count, err := collection.Find(bson.M{"chat_id": chatID}).Count()  // Считаем количество записей с заданным ChatID
	if err != nil || count == 0 { 
		return false, err
	} else {
		return true, err
	}
}

// Получение пользователя
func (connection *DatabaseConnection) GetUser(chatID int64) (User, error) {
	var result User
	find, err := connection.Find(chatID)  // Сначала проверяем, существует ли он
	if err != nil {
		return result, err
	}
	if find {  // Если да -> получаем
		collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)  
		err = collection.Find(bson.M{"chat_id": chatID}).One(&result)
		return result, err
	} else {  // Нет -> возвращаем NotFound
		return result, mgo.ErrNotFound
	}
}

// Создание пользователя
func (connection *DatabaseConnection) CreateUser(user User) error {
	collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)
	err := collection.Insert(user)
	return err
}

// Обновление номера мобильного телефона
func (connection *DatabaseConnection) UpdateUser(user User) error {
	collection := connection.DB.C(conf.MONGODB_COLLECTION_USERS)
	err := collection.Update(bson.M{"chat_id": user.Chat_ID}, &user)
	return err
}

Это все функции, которые нам пригодятся. Для бота осталось написать функции продолжения обработки сообщения и анализа контакта:
func (telegramBot *TelegramBot) analyzeUser(update tgbotapi.Update) {
	chatID := update.Message.Chat.ID
	user, err := Connection.GetUser(chatID)  // Вытаскиваем данные из БД для проверки номера
	if err != nil {
		msg := tgbotapi.NewMessage(chatID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
		return
	}
	if len(user.Phone_Number) > 0 {
		msg := tgbotapi.NewMessage(chatID, "Ваш номер: " + user.Phone_Number)  // Если номер у нас уже есть, то пишем его
		telegramBot.API.Send(msg)
		return
	} else {
		// Если номера нет, то проверяем ждём ли мы контакт от этого ChatID
		if telegramBot.findContactRequestID(chatID) {
			telegramBot.checkRequestContactReply(update)  // Если да -> проверяем
			return
		} else {
			telegramBot.requestContact(chatID)  // Если нет -> запрашиваем его
			return
		}
	}
}

// Проверка принятого контакта
func (telegramBot *TelegramBot) checkRequestContactReply(update tgbotapi.Update) {
	if update.Message.Contact != nil {  // Проверяем, содержит ли сообщение контакт
		if update.Message.Contact.UserID == update.Message.From.ID {  // Проверяем действительно ли это контакт отправителя
			telegramBot.updateUser(User{update.Message.Chat.ID, update.Message.Contact.PhoneNumber}, update.Message.Chat.ID)  // Обновляем номер
			telegramBot.deleteContactRequestID(update.Message.Chat.ID)  // Удаляем ChatID из списка ожидания
			msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Спасибо!")
			msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(false)  // Убираем клавиатуру
			telegramBot.API.Send(msg)
		} else {
			msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Номер телефона, который вы предоставили, принадлежит не вам!")
			telegramBot.API.Send(msg)
			telegramBot.requestContact(update.Message.Chat.ID)
		}
	} else {
		msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Если вы не предоставите ваш номер телефона, вы не сможете пользоваться системой!")
		telegramBot.API.Send(msg)
		telegramBot.requestContact(update.Message.Chat.ID)
	}
}

// Обновление номера мобильного телефона пользователя
func (telegramBot *TelegramBot) updateUser(user User, chatID int64) {
	err := Connection.UpdateUser(user)
	if err != nil {
		msg := tgbotapi.NewMessage(chatID, "Произошла ошибка! Бот может работать неправильно!")
		telegramBot.API.Send(msg)
		return
	}
}

// Есть ChatID в листе ожидания?
func (telegramBot *TelegramBot) findContactRequestID(chatID int64) bool {
	for _, v := range telegramBot.ActiveContactRequests {
		if v == chatID {
			return true
		}
	}
	return false
}

// Удаление ChatID из листа ожидания
func (telegramBot *TelegramBot) deleteContactRequestID(chatID int64) {
	for i, v := range telegramBot.ActiveContactRequests {
		if v == chatID {
			copy(telegramBot.ActiveContactRequests[i:], telegramBot.ActiveContactRequests[i + 1:])
			telegramBot.ActiveContactRequests[len(telegramBot.ActiveContactRequests) - 1] = 0
			telegramBot.ActiveContactRequests = telegramBot.ActiveContactRequests[:len(telegramBot.ActiveContactRequests) - 1]
		}
	}
}

Наш бот готов к работе! Осталось только его запустить. Для этого в main.go пишем:
var telegramBot src.TelegramBot

func main() {
	src.Connection.Init()  // Инициализация соединения с БД
	telegramBot.Init()  // Инициализация бота
	telegramBot.Start() 
}

В итоге у нас получился бот, запрашивающий у пользователя его номер, который в дальнейшем будет использоваться в системе. Бота можно и нужно улучшить, добавить интеграцию с сайтом, вход по одноразовым паролям, и.т.д.
Несомненно у данной системы есть минусы, самый очевидный из которых: зависимость от Telegram (изменение API, блокировка). По мне это хорошая альтернатива e-mail и sms, будете ли вы использовать её — решать вам. Спасибо за внимание!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331502/


Метки:  

Эффективная DI библиотека на Swift в 200 строк кода

Понедельник, 26 Июня 2017 г. 23:01 + в цитатник
Библиотека EasyDi содержит контейнер зависимостей для Swift. Синтаксис этой библиотеки был специально разработан для быстрого освоения и эффективного использования. Она умещается в 200 строк, при этом умеет все, что нужно взрослой Di библиотеке:

— Создание объектов и внедрение зависимостей в существующие
— Разделение на контейнеры — Assemblies
— Типы разрешения зависимостей: граф объектов, синглетон, прототип
— Разрешение циклических зависимостей
— Подмена объектов и конктесты зависимостей для тестов

В EasyDi нет разделения на register/resolve. Вместо этого зависимости описываются вот так:
var apiClient: IAPIClient {
  return define(init: APIClient()) {
    $0.baseURl = self.baseURL
  }
}


Cocoapods / EasyDi
Github / EasyDi

Под катом очень краткое описание «Зачем DI и что это», также примеры использования библиотеки:
  • Как использовать и типы зависимостей
  • Как тестировать c подменой объектов
  • Как можно это использовать для A/B тестов
  • Как собрать VIPER-модуль



Зачем DI и что это?(очень кратко)



Инверсия зависимостей в проекте очень важна, если он содержит в себе больше 5 экранов и будет поддерживаться больше года.
Вот три базовых сценария, где DI делает жизнь лучше:
  • Параллельная разработка. Один разработчик сможет заниматься UI, а второй данными, если заранее договорятся о протоколе работы. UI тогда может разрабатываться с тестовыми данными, а слой данных вызываться из тестового UI.
  • Тесты. Подменяя сетевой слой на объекты с фиксированными ответами, можно проверить все варианты поведения UI, в том числе в случае ошибок.
  • Рефакторинг. Сетевой слой можно заменить на новый, быстрый с кэшем и другим API, если оставить без изменений протокол с UI.


Суть DI можно так описать одним предложением:

Зависимости для объектов надо закрыть протоколами и передать в объект снаружи.


Т.е. вместо
class OrderViewController {
  func didClickShopButton(_ sender: UIButton?) {
    APIClient.sharedInstance.purchase(...)
  }
}


Стоит использовать

protocol IPurchaseService {
  func perform(...)
}

class OrderViewController {
  var purchaseService: IPurchaseService?
  func didClickShopButton(_ sender: UIButton?) {
    self.purchaseService?.perform(...)
  }
}


Подробнее с принципом инверсии зависимостей и концепцией SOLID можно познакомиться
тут (objc.io #15 DI) и тут (wikipedia. SOLID).

Как работать с EasyDi (Простой пример)



Простой пример использования библиотеки: убрать из ViewController работу с сетью в сервисы и разместить их создание и зависимости в отдельном контейнере. Это простой и эффективный способ начать деление приложения на слои. В примере рассмотрим сервис и контроллер из примера выше.

Пример кода сервиса и контроллера
protocol IPurchaseService {
  func perform(with objectId: String, then completion: (success: Bool)->Void)
}    

class PurchaseService: IPurchaseService {

  var baseURL: URL?
  var apiPath = "/purchase/"
  var apiClient: IAPIClient?
  
  func perform(with objectId: String, then completion: (_ success: Bool) -> Void) {
    guard let apiClient = self.apiClient, let url = self.baseURL else {
      fatalError("Trying to do something with uninitialized purchase service")
    }
    let purchaseURL = baseURL.appendingPathComponent(self.apiPath).appendingPathComponent(objectId)
    let urlRequest = URLRequest(url: purchaseURL)
    self.apiClient.post(urlRequest) { (_, error) in
      let success: Bool = (error == nil)
        completion( success )
    }
  }
}


Контроллер:
class OrderViewController {

  var purchaseService: IPurchaseService?
  var purchaseId: String?
  
  func didClickShopButton(_ sender: UIButton?) {

    guard let purchaseService = self.purchaseService, let purchaseId = self.purchaseId else {
      fatalError("Trying to do something with uninitialized order view controller")
    }

    self.purchaseService.perform(with: self.purchaseId) { (success) in
      self.presenter(showOrderResult: success)
    }
  }
}



Зависимости сервиса:
class ServiceAssembly: Assembly {
  
  var purchaseService: IPurchaseService {
    return define(init: PurchaseService()) {
      $0.baseURL = self.apiV1BaseURL
      $0.apiClient = self.apiClient
    }
  }

  var apiClient: IAPIClient {
    return define(init: APIClient())
  }

  var apiV1BaseURL: URL {
    return define(init: URL("http://someapi.com/")!)
  }
}


И вот так мы внедряем сервис в контроллер:
var orderViewAssembly: Assembly {
  
  var serviceAssembly: ServiceAssembly = self.context.assembly()

  func inject(into controller: OrderViewController, purchaseId: String) {
    define(init: controller) {
      $0.purchaseService = self.serviceAssembly.purchaseService
      $0.purchaseId = purchaseId
    }
  }
}


Теперь можно поменять класс сервиса не залезая во ViewController.

Типы разрешения зависимостей (Пример средней сложности)



ObjectGraph



По-умолчанию все зависимости разрешаются через граф объектов. Если объект уже есть в стеке текущего графа объектов, то он используется снова. Это позволяет внедрить один и тот же объект в несколько, а также разрешить циклические зависимости. Для примера возьмём объекты A,B и C со ссылками A->B->C.(Не будем обращать внимания на RetainCycle, он нужен для полноты примера).
class A {
  var b: B?
}

class B {
  var c: C?
}

class C {
  var a: A?
}


Вот так выглядит Assembly и вот такой граф зависимостей для двух запросов A.
class ABCAssembly: Assembly {

  var a:A {
    return define(init: A()) {
      $0.b = self.B()
    }
  }

  var b:B {
    return define(init: B()) {
      $0.c = self.C()
    }
  }

  var c:C {
    return define(init: C()) {
      $0.a = self.A()
    }
  }
}

var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a


Получилось два независимых графа.

Singleton



Но бывает так, что нужно создать один объект, который потом будет использоваться везде, например система аналитики или хранилище. Использовать классический Singleton с SharedInstance не стоит, т.к. будет невозможно его подменить. Для этих целей в EasyDi есть scope: singleton. Этот объект создаётся один раз, в него один раз внедряются зависимости и больше EasyDi его не меняет, только возвращает. Для примера сделаем B синглетоном.
class ABCAssembly: Assembly {
  var a:A {
    return define(init: A()) {
      $0.b = self.B()
    }
  }

  var b:B {
    return define(scope: .lazySingleton, init: B()) {
      $0.c = self.C()
    }
  }

  var c:C {
    return define(init: C()) {
      $0.a = self.A()
    }
  }
}

var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a


На этот раз получился один граф объектов, т.к. B стал общим.

Prototype



И иногда требуется при каждом обращении получать новый объект. На примере объектов ABC для A-прототипа это будет выглядеть так:
class ABCAssembly: Assembly {
  var a:A {
    return define(scope: .prototype, init: A()) {
      $0.b = self.B()
    }
  }

  var b:B {
    return define(init: B()) {
      $0.c = self.C()
    }
  }

  var c:C {
    return define(init: C()) {
      $0.a = self.A()
    }
  }
}

var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a


Получается, что два графа объектов дают 4 копии объекта A

Важно понять, что это точка входа в граф и другие зависимости не надо делать прототипами. Если объединить прототипы в цикл, то стек переполнится и приложение упадёт.

Патчи и контексты для тестов (Сложный пример)



При тестировании важно сохранять независимость тестов. В EasyDi это обеспечивается контекстами Assemblies. Например, интеграционные тесты, где используются синглетоны. Используются они вот так:
let context: DIContext = DIContext()
let assemblyInstance2 = TestAssembly.instance(from: context)

При этом важно следить за тем, чтобы контексты у совместно используемых Assemblies совпадали.
class FeedViewAssembly: Assembly {

  lazy var serviceAssembly:ServiceAssembly = self.context.assembly()

}


Другая важная часть тестирования — это моки и стабы, т.е объекты с известным поведением. При известных входных данных тестируемый объект выдаёт известный результат. Если не выдаёт, значит тест не пройден. Подробнее про тестирование можно узнать тут (objc.io #15 весь). А вот так можно подменить объект:
protocol ITheObject {
  var intParameter: Int { get }
}

class MyAssembly: Assembly {

  var theObject: ITheObject {
    return define(init: TheObject()) {
      $0.intParameter = 10
    }
  }
}

let myAssembly = MyAssembly.instance()
myAssembly.addSubstitution(for: "theObject") { () -> ITheObject in
  let result = FakeTheObject()
  result.intParameter = 30
  return result
}

Теперь свойство theObject будет возвращать новый объект другого типа с другим intParameter.

про A / B тесты

про A / B тесты


Этот же механизм можно использовать для a/b тестирования в приложении. Например вот так:
let FeatureAssembly: Assembly {
  
  var feature: IFeature {
    return define(init: Feature) {
      ...
    }
  }
}

let FeatureABTestAssembly: Assembly {

  lazy var featureAssembly: FeatureAssembly = self.context.assembly()

  var feature: IFeature {
    return define(init: FeatureV2) {
      ...
    }
  }

  func activate(firstTest: Bool) {
    if (firstTest) {
      self.featureAssembly.addSubstitution(for: "feature") {
        return self.feature
      }
    } else {
      self.featureAssembly.removeSubstitution(for: "feature")
    }
  }
}

Здесь для теста создается отдельный контейнер, который создает второй вариант объекта и позволяет включить/выключить подстановку этого объекта.


Внедрение зависимостей в VIPER

Внедрение зависимостей в VIPER



Бывает так, что надо внедрить зависимости в существующий объект, а от него тоже кто-то зависит. Самый очевидный пример — это VIPER, когда во ViewController надо добавить Presenter, а он сам должен получить ссылку на ViewController.

Для этого в EasyDi есть ‘ключи’ и плейсхолдеры с помощью которых можно возвращать один и тот же объект из разных методов. Выглядит это так:

сlass ModuleAssembly: Assembly {

  func inject(into view: ModuleViewController) {
    return define(key: "view", init: view) {
      $0.presenter = self.presenter
    }
  }

  var view: IModuleViewController {
    return definePlaceholder()
  }

  var presenter: IModulePresenter {
    return define(init: ModulePresenter()) {
	  $0.view = self.view
      $0.interactor = self.interactor
    }
  }

  var interaction: IModuleInteractor {
    return define(init: ModuleInteractor()) {
	  $0.presenter = self.presenter
      ...
    }
  }
}


Здесь для внедрения зависимостей во ViewController используется метод inject, который связан ключом со свойством viewController. Теперь это свойство возвращает объект, переданный в метод inject. И только при разрешении зависимостей графа объектов, который начинается с метода inject.


Вместо заключения


У меня не было цели упихать все в 200 строк, просто так получилось. Наиболее влияние на эту библиотеку оказал Typhoon, очень хотелось иметь что-то похожее, но на Swift и попроще.

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

Библиотека упакована в 1 файл, чтобы проще было добавлять в переходные проекты, где ещё не используется use_frameworks, но Swift уже есть.

Ссылки на библиотеку:


Текущая версия '1.1.1'
pod 'EasyDi', '~>1.1'

Должна одинаково хорошо работать на Swift 3/4, в iOS 8+.
На iOS 7 — не знаю, не могу проверить.

А депо-приложение — читалка комиксов XKCD.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331710/


Метки:  

[Из песочницы] Альтернативы блокчейну для ведения защищённых реестров

Понедельник, 26 Июня 2017 г. 19:44 + в цитатник

Технология «блокчейн» прекрасна и перспективна. Всё в ней было бы совсем замечательно, если бы несколько досадных нюансов:

  1. Очень долго. Время добавления транзакции в цепочку биткоина, например, оценивается от минуты до получаса. В Ethereum добавляется быстрее, но в любом случае довести время до долей секунды невозможно. Нечего и думать о том, чтобы сделать добавление данных в блокчейн частью OLTP-транзакции.
  2. Майнинг — это очень ресурсоёмко. Он, собственно, и нужен для того, чтобы добавить в архитектуру вычислительную сложность.
  3. Очень дорого. Следствие ресурсоёмкости.
  4. Технология отвратительно масштабируется как вверх, так и вниз. Если нужно построить систему, которая будет регистрировать миллиарды записей ежедневно, блокчейн не годится. Также блокчейн будет стрельбой из пушки по воробьям, если его пытаться приспособить для надёжного логирования какой-нибудь мелкой ерунды.

Хотелось бы иметь технологию, которая бы одновременно и реестры позволяла вести непрошибаемо надёжным образом, и была бы как-то попроще и подешевле.

Модельная ситуация (художественный вымысел по мотивам реальных событий)
В одной крупной производственно-дистрибьюторской компании был большой департамент продаж, в котором, как водится, бонусы менеджерам зависели от объёмов проданного.

Самые сладкие объёмы продаж делаются на самом ходовом и, как следствие, дефицитном товаре. Менеджеры-продажники — ребята ушлые. Они быстро сообразили, что самый верный способ иметь хорошие бонусы — постоянно мониторить поставки, и как только на складе появляется дефицит, быстренько резервировать его под себя. Создавать фиктивный «Заказ покупателя», набивать его дефицитом и говорить всем, что это клиент, который пока что думает, что ему нужно взять ещё (в общем, нормальная ситуация). Потом, когда появляется реальный запрос на дефицит, быстренько убирать требуемое количество из фиктивного счёта, и в ту же секунду добавлять в реальный.

В результате бизнес несёт убытки. На складе неделями валяется товар, который должен был бы уйти в первый же день. Департамент продаж вместо того, чтобы заниматься поиском новых клиентов и выстраиванием взаимоотношений с ними, целыми днями мониторит поставки. Склоки, нервотрёпка, взаимные упрёки и обиды. С этим что-то надо было сделать, но так, чтобы это не повредило основному бизнес-процессу. Решили для начала логировать операции резервирования. Отследить ситуации, когда дефицит резервируется «заказом покупателя», но потом это резервирование не превращается в продажу этим же документом. Тех, кто этим больше всех злоупотребляет, вешать на позорном столбе.

Поначалу помогло, но потом начал наблюдаться странный эффект: показатели зависания дефицита на складе вернулись к прежним, хотя по логам всё было шоколадно. Оказалось, что что в реализации не была учтена сила обаяния менеджеров по продажам. Ради высоких бонусов они повадились сговариваться с системными администраторами, чтобы те вычищали из логов компромат. В ход пошёл весь арсенал средств — подкуп, шантаж, флирт, женские слёзы. Парням из службы администрирования оказалось нечего противопоставить такой атаке.

Встал вопрос о том, чтобы техническими средствами сделать невозможным искажение лога задним числом. Посмотрели в сторону блокчейна, но выяснили, что пропускать резервирование товара через популярные блокчейн-платформы слишком накладно, а городить отдельную серверную стойку под собственный блокчейн для улаживания дрязг в департаменте продаж тоже никакого желания нет. Что делать — не понятно.

В принципе, на этом можно было бы публикацию закончить и предложить уважаемой публике пофантазировать в комментах о том, как можно сделать супернадёжную запись трензакций в условиях тотального недоверия всех ко всем без того, чтобы бессмысленно прожигать процессорные мощности никчёмным подбором хеш-функций. Но для того, чтобы единственным ответом на вопрос не было банальное «это невозможно» или что-нибудь про сайдчейны, попробую предложить свой вариант.

Сразу оговорюсь, что речь вовсе не о том, «как нам реорганизовать биткоин». Пусть биткоин остаётся таким, какой он есть, а мы поговорим о том, как сделать что-то функционально аналогичное блокчейну, но пригодное к использованию для самых разных нужд в частности в корпоративных системах. Дешёвое и сердитое.

Для начала внимательно посмотрим на саму задачу. Во-первых, мы уже имеем сам защищаемый от фальсификации реестр. Во-вторых, нам не очень важна распределённость системы. Удобнее всего, если система защиты реестра будет централизованной. В идеале — на том же самом сервере, что и сам реестр. Распределённость реестра и системы его защиты тоже может быть во многих случаях полезна, но начать нужно с простого. С принципа защиты.

Система защиты реестра в инфраструктуре корпоративной системы:

Априорно считаем, что всё происходящее на серверах — в полной власти администраторов, и эти администраторы с их человеческими страстями — слабое звено. Поэтому функция контроля целостности реестра частично возлагается на рабочие места пользователей. Реализуется принцип «не доверяй, проверяй». Администратор может сделать что угодно с расположенными на сервере данными, но он не в силах отследить, куда и по каким компьютерам (а также смартфонам, планшетам, флешкам и т.п.) расползлись контрольные суммы, фиксирующие целостность реестра. В клиентское приложение (или, возможно, в отдельное специальное контролирующее приложение) встраивается функция, которая при обнаружении факта нарушения целостности реестра зажигает красную лампочку и позволяет сформировать криптографически достоверное обвинительное заключение.

Защищаемый реестр и его контрольные суммы:



Для каждой записи реестра подсчитывается собственный хеш (H0i), а также автоматически наращивается бинарное дерево хешей второго, третьего и так далее порядков (H1, H2,...). После того, как очередная запись занесена в реестр, клиентскому приложению возвращается сама занесённая запись, а также такой набор хешей более высоких уровней, чтобы он полностью покрывал все предыдущие записи реестра. Например, при добавлении шестой записи возвращается D5, H05, H12 и H20 (удобнее смотреть на картинке ниже). При добавлении 15-й записи, соответственно, возвращается D14, H014, H16, H22 и H30. Получив ответ от сервера, клиентское приложение:

  1. Проверяет соответствие Di тому, что оно собиралось занести в реестр.
  2. Самостоятельно рассчитывает H0i и проверяет, равно ли получившееся тому, что прислал сервер.
  3. Запоминает себе в локальную базу полученный от сервера ответ.

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

Экспресс-проверка неизменности собственных записей клиента

Когда клиент добавил в реестр очередную запись, он может сверить новый полученный срез хешей со срезами, полученными при предыдущих обращениях. Поскольку с предыдущего обращения могли нараститься дополнительные уровни, для сверки хешей может потребоваться некоторое количество дополнительны данных от сервера. Например, если клиент сначала добавил D5, а потом добавил D14, то для сверки H20 с H30 он запрашивает с сервера значение H13, затем на основании H12 и H13 вычисляет H21, после чего на основании H20 и H21 рассчитывает H30, которое сравнивает с полученным. Если значения не совпадают, включается красная лампочка и сигнал тревоги.


Серым цветом показаны данные, отсутствующие на клиенте. При добавлении первой своей записи (D5) клиент в своей локальной базе сохранил данные, выделенные зелёным, а при добавлении следующей записи (D14) получил данные, выделенные синим. Выделенное жёлтым запрашивается с сервера защиты дополнительно.

Сплошная проверка участка реестра

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

В результате имеем:

  1. Скорость добавления данных в реестр. Майнить коины оказывается незачем. Увеличение производительности вплоть до того, что данные в защищённый реестр можно записывать внутри OLTP-транзакции.
  2. Надёжность. Незаметно удалить запись из реестра невозможно. Сложность искажения данных равна сложности подбора полной хеш-коллизии.
  3. Простота реализации. Прекрасная масштабируемость «вниз».
  4. Прекрасная масштабируемость «вверх». Даже если в реестре сто миллиардов записей, количество уровней бинарного дерева оказывается равным 36. Если используется хеширование SHA-256, передаваемые с сервера данные хешей составляют менее 2 килобайт.
  5. Хоть ведение реестра становится централизованным (что для случая криптовалюты в стиле биткоина не очень хорошо), вполне возможна распределённая архитектура на основе репликации и оперативного переключения со скомпрометированных узлов на те, которые не были уличены в подлоге.

P.S. Про исходники на гитхабе, пожалуйста, не спрашивайте. Их нет. Идея уходит в публикацию с колёс.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331706/


Метки:  

[Из песочницы] Что будет если скрестить React и Angular?

Понедельник, 26 Июня 2017 г. 19:19 + в цитатник

Akili — javascript фреймворк, который появился под влиянием таких решений как React, Angular, Aurelia и в меньшей степени некоторых других. Целью было объединить все лучшее, что я вижу в них и максимально все упростить.

Нравится React, но отталкивает JSX? Любите Angular, но надоела всякая магия?

Тогда вам стоит попробовать это.

Я убежден, что наилучший способ в чем-то разобраться это практика. Поэтому начну описание сразу же с примеров. Они написаны так, как если бы мы компилировали код с помощью Babel (es2015 + stage-0).

Первые шаги


import Akili from 'akili';

class MyComponent extends Akili.Component {
  constructor(el, scope) {
    super(el, scope);
    scope.example = 'Hello World';
  }
}

Akili.component('my-component', MyComponent); // регистрируем компонент

document.addEventListener('DOMContentLoaded', () => {
  Akili.init(); // инициализируем фреймворк
});


  ${ this.example }


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

Во-первых, область видимости компонента разделена от области видимости разметки. То есть, можно спокойно наследовать компоненты и это никак не отразиться на этой самой разметке.

class MySecondComponent extends MyComponent  {
 constructor(...args) {
    super(...args);
    this.scope.example = 'Goodbye World';
  }
  myOwnMethod() {}
}

Akili.component('my-second-component', MySecondComponent)


  ${ this.example }
  ${ this.example }


За область видимости разметки отвечает свойство компонента scope. Это специальный объект, который вы можете заполнить необходимыми данными и отображать их в шаблонах с помощью выражений вида ${ this.example }, где this и есть этот самый scope. На самом деле в скобках может быть любое javascript выражение.

Во-вторых, области видимости разметки также наследуются. Добавим в scope первого компонента новое значение:

class MyComponent extends Akili.Component {
  constructor(el, scope) {
    super(el, scope);
    scope.example = 'Hello World';
    scope.test = 'Test';
  }
}

Тогда разметка ниже:


  
     ${ this.example }
     ${ this.example } - ${ this.test }
   


После компиляции будет выглядеть как:


  
     Hello World
     Goodbye World - Test
   


В-третьих, синхронизация логики компонента с его шаблоном происходит путем лишь изменения переменной scope в любой момент времени.

class MyComponent extends Akili.Component {
  constructor(...args) {
    super(...args);
    this.scope.example = 'Hello World';

    setTimeout(() => {
      this.scope.example = 'Goodbye World';
    }, 1000);
  }
}

Через секунду значение переменной изменится и в объекте и в шаблоне.

Lifecycle в двух словах, в сравнении с React


.constructor(el, scope)
Прежде всего, поскольку любой компонент это простой javascript класс, будет вызван конструктор. Он получает в аргументы html элемент, к которому будет привязан компонент и объект scope. Здесь вы можете делать с элементом любые изменения, либо отменить компиляцию, в случаи необходимости, вызовом метода .cancel().

.created()
Если компиляция компонента не была отменена, то вы попадаете сюда. Этот метод фактически ничем не отличается от конструктора. В React, похожую функцию выполняет componentWillMount.

.compiled()
Здесь компонент скомпилирован, в шаблонах вместо выражений уже соответствующие значения.
В React это componentDidMount. Вы имеете доступ ко всем родительским элементам, которые к этому моменту гарантированно скомпилированы тоже.

.resolved()
Этот метод, насколько я знаю, не имеет аналогов в известных мне фреймфорках.
Дело в том, что Akili позволяет использовать при компиляции асинхронные операции, если это нужно. К ним относятся некоторые системные и любые кастомные операции. Например, загрузка шаблона компонента из файла:

class MyComponent extends Akili.Component {
  static templateUrl = '/my-component.html';

  constructor(...args) {
    super(...args);
    this.scope.example = 'Hello World';
  }
}

Или любая асинхронная операция, которую мы выполним сами:

class MyComponent extends Akili.Component {
  static templateUrl = '/my-component.html';

  constructor(...args) {
    super(...args);
    this.scope.example = 'Hello World';
  }

  compiled() {
     return new Promise((res) => setTimeout(res, 1000));
  }
}

В методе compiled вы можете вернуть промис, тогда resolved будет ждать выполнения ВСЕХ асинхронных операций. При этом сама компиляции будет происходить синхронно.

Другими словами в методе resolved вы можете быть уверены, что скомпилированы абсолютно все дочерние элементы, любого уровня вложенности, в том, числе содержащие какие-либо асинхронные операции.

.removed()
Вызывается при удалении компонента. Аналог — componentDidUnmount.

.changed(key, value)
Вызывается при изменении любого атрибута компонента. Аналог — componentWillReceiveProps.
Это очень важная часть фреймфорка, поэтому опишу ее более подробно в отдельной секции ниже.

Универсальность, изоляция, модульность компонентов


Очень важно, чтобы компонент мог быть полностью изолирован и вообще не зависел от внешних условий. Вот пример такого компонента:

import Akili from 'akili';

class NineComponent extends Akili.Component {
  static template = '${ this.str }';

  static define() {
     Akili.component('nine', NineComponent);
  }
  constructor(...args) {
    super(...args);
    this.scope.str = '';
  } 
  compiled() {
     this.attrs.hasOwnProperty('str') && this.addNine(this.attrs.str);
  }
  changed(key, value) {
     if(key == 'str') {
        this.addNine(value);
     }
  }
  addNine(value) {
    this.scope.str = value + '9';
  }
}

Добавим его к предыдущим примерам:

import NineComponent from './nine-component';

NineComponent.define();
Akili.component('my-component', MyComponent); 

document.addEventListener('DOMContentLoaded', () => {
  Akili.init();
});


  
     
    


Итак, вот что мы получим после компиляции:


  
    Hello World9
    


Обратите внимание, NineComponent получился абсолютно обособленным. Он похож на функцию, которая может принимать какие-то аргументы и что-то с ними делать. В данном случаи просто добавляет цифру 9 в конец переданной строки и отображает ее.

Можно провести аналогию между атрибутами в Akili и свойствами в React.
this.attrs => this.props. Они выполняют одну и туже роль, но есть мелкие различия:

В Akili свойство attrs как и scope является Proxy, то есть можно добавить, изменить или удалить атрибут html элемента, делая соответствующие операции с каким-то свойством данного объекта. Свойства объекта attrs синхронизируются с атрибутами элемента.

Вы можете использовать атрибуты для биндинга. В примере выше, если переменная области видимости this.example компонента MyComponent изменится, то будет вызван метод changed у NineComponent. Обратите внимание, мы не сделали для этого ничего особенного. Выражение в атрибуте str ничем не отличается от примеров в начале, где мы просто отображали значение в шаблоне.

Для удобства можно использовать сокращенную версию changed.

class NineComponent extends Akili.Component { 
  changed(key, value) {
     if(key == 'str') {
        this.addNine(value);
     }
  }
}

class NineComponent extends Akili.Component { 
  changedStr(value) {
     this.addNine(value);
  }
}

Примеры выше эквиваленты. Чтобы не плодить гору ифов или кэйсов, проще писать сразу нужный метод. Принцип именования прост: changed + название атрибута кэмел кейсом с заглавной буквы.

События


Здесь все просто, добавляем тире после on, а дальше все как обычно. Изменим наш первоначальный пример:

class MyComponent extends Akili.Component {
  static events = ['timeout'];

  constructor(...args) {
    super(...args);
    this.scope.example = 'HelloWorld';
    this.scope.sayGoodbye = this.sayGoodbye;
  }
  compiled() {
      setTimeout(() => this.attrs.onTimeout.trigger(9), 5000);
  }
  sayGoodbye(event) {
      console.log(event instanceof Event); // true
      this.scope.example = 'Goodbye World';
  }
}


  
    
    ${ this.example }
    


Система событий основана на нативной. В примере выше видно, что вы также можете создавать и вызывать свои кастомные события.

Работа с массивами


class MyComponent extends Akili.Component {
  constructor(...args) {
    super(...args);

    this.scope.data = [];

    for (let i = 1; i <= 10; i++) {
      this.scope.data.push({ title: 'value' + i });
    }
  }
}


  
    ${ this.loopIndex } => ${ this.loopKey} => ${ this.loopValue.title  }
  



  
  • ${ this.loopValue }


Дополнительно


Из коробки Akili также имеет роутер, библиотечку для совершения ajax запросов, множество системных компонентов для работы с циклами, формами, возможность прикрутить серверный рендеринг и.т.д, в документации вы можете найти подробное описание.

Данная статься написана чтобы познакомить вас с Akili, я постарался раскрыть в целом какие-то технические моменты, но здесь не уместилась даже пятая часть того, что в себе содержит фреймворк. Гораздо больше информации есть в документации, ну и если будет интерес, то начну раскрывать тему глубже в других статьях.

Фреймворк пока в бете, пробуйте, смотрите )
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331704/


Метки:  

Выпуск#4: ITренировка — актуальные вопросы и задачи от ведущих компаний

Понедельник, 26 Июня 2017 г. 18:56 + в цитатник
Всем привет! На этой неделе мы решили немного поменять формат. Теперь кейсы будут ориентированы на более конкретных специалистов. Новый выпуск будет интересен, в первую очередь, разработчикам PHP. Ответы ищите на следующей неделе в комментариях под этим постом. А следующий выпуск будет для разработчиков Java.

1. Рассмотрим следующий код:

$str1 = 'yabadabadoo';
$str2 = 'yaba';
if (strpos($str1,$str2)) {
	echo "\"" . $str1 . "\" contains \"" . $str2 . "\"";
} else {
	echo "\"" . $str1 . "\" does not contain \"" . $str2 . "\"";
}

На выходе получим:

"yabadabadoo" does not contain "yaba"

Почему? Как этот код может быть исправлен для корректной работы?

2. В чем отличие между echo и print в PHP?

3. Что такое PEAR в php?

4. Чему будет равно $x при условии, что $x = 3 + «15%» + "$25"?

5. Каковы будут значения $ a и $ b после выполнения кода ниже? Поясните свой ответ

$a = '1';
$b = &$a;
$b = "2$b";
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331702/


Метки:  

Таблицы! Таблицы? Таблицы…

Понедельник, 26 Июня 2017 г. 18:34 + в цитатник

В статье я покажу стандартную табличную разметку, какие у неё есть альтернативы. Дам пример собственной таблицы и разметки, а также опишу общие моменты её реализации.


Стандартная HTML4 таблица


Когда появилась необходимость в HTML разметке показывать таблицы — изобрели тег

.
Что же даёт нам таблица в браузере? Вот несколько основных "фич":

В данном случае вычисляется процентное соотношение каждого столбца к общей ширине и каждый столбец растягивается соответственно процентному соотношению.


В первом примере ширина всей таблицы (примерно) = 387px, колонки Company = 206px, колонки Contact = 115px.


В процентах Company = 206px/387px * 100% = 53%, Contact = 115px/387px * 100% = 30%.


Теперь когда содержимое таблицы растянулось, ширина всей таблицы (примерно на моем экране) = 1836px, колонки Company = 982px, колонки Contact = 551px.


В процентах Company = 982px/1836px * 100% = 53%, Contact = 551px/1836px * 100% = 30%.



Можно "дожать" таблицу указав ей CSS свойство table-layout: fixed. Описание к свойству.


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


Если мы не указали ширину столбцов, тогда при "сломанной" таблице, ширина каждого столбца = вся ширина / количество столбцов.


  • Схлопывание (наложение) границ ячеек/столбцов border-collapse: collapse, если мы указали границы для ячеек. Т.е. в местах соприкосновения ячеек, не будет двойных граничных линий.


  • Группировка шапки. Реализуется атрибутами colspan, rowspan.

Использование стандартной таблицы


Во всех вышеприведенный примерах в разметке таблицы я использовал сокращенную разметку:


Cокращенная разметка
Header 1 Header 2
1.1 1.2
2.1 2.2

Однако можно использовать "каноничную" разметку:


Каноничная разметка
Header 1 Header 2
1.1 1.2
2.1 2.2

Если нужна таблица без шапки и в то же время нам необходимо контроллировать ширину столбцов:


Разметка без шапки
1.1 1.2
2.1 2.2

Чаще всего нам в разметке необходимо получить следующее. У нас есть некий контейнер с заданной шириной или с заданной максимальной шириной. Внутри него мы хотим вписать таблицу.


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


Но ни в коем случае мы не хотим, чтобы таблица сделала наш контейнер шире чем мы задали.


По этой ссылке можно уведеть контейнер с таблицей в действии. Если мы будем сужать контейнер, то в тот момент, когда таблица уже больше не сможет сужаться — появиться скролл.


Подстройка таблицы


Задание ширины таблицы и столбцов


Первая дилемма с которой сталкиваются фронт-энд разработчики — это задавать или не задавать ширину столбцов.


Если не задавать, тогда ширина каждого столбца будет вычисляться в зависимости от содержимого.


Исходя из логики, можно понять, что в этом случае браузеру нужно два прохода. На первом он просто отображает все в таблице, подсчитывает ширину столбцов (мин, макс). На втором подстраивает ширину столбцов в зависимости от ширины таблицы.


Со временем вам скажут что таблица выглядит некрасиво, т.к. один из столбцов слишком широкий и


вот в этом столбце нам надо показать больше текста чем в этом, а у нас наоборот

И самая распространенная "фича":


  • это сокращение текста в ячейке с помощью ...

Т.е. если текст в ячейке вылазит за ширину колонки, то его необходимо сокращать и в конце добавлять ....


Первое разочарование, что если не задавать ширину столбцов, то сокращение не работает. В этом есть своя логика, т.к. на первом проходе браузер высчитывает мин/макс ширину колонки без сокращения, а тут мы пытаемся сократить текст. Необходимо либо все пересчитать повторно, либо игнорировать сокращение.


Сокращение реализуется просто, необходимо указать CSS свойства для ячейки:


CSS
td {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

И соответственно задать ширину колонки. По этой ссылке можно увидеть, что все настроено, но сокращение не работает.


В спецификации есть заметка, немного объясняющая, почему сокращение не работает:


If column widths prove to be too narrow for the contents of a particular table cell, user agents may choose to reflow the table

Опять же сужаться таблица будет до минимальной ширины содержимого. Но если применить свойство table-layout: fixed то таблица начнёт "слушаться" и сокращение заработает. Но автоподстройка ширины столбцов уже не работает.


Задание прокрутки таблицы


Вышеприведенный пример будет работать со скроллом и пользоваться этим можно. Однако возникает следующее требование:


здесь нам надо сделать, чтобы шапка таблицы оставалась на месте, а тело прокручивалось

Вторая дилемма с которой сталкиваются фронт-энд разработчики:


  • задание прокрутки/скролла в таблице

В спецификации таблицы есть прямое указание, что тело таблицы может быть с шапкой и подвалом. Т.е. шапка и подвал всегда видимы.


User agents may exploit the head/body/foot division to support scrolling of body sections independently of the head and foot sections. When long tables are printed, the head and foot information may be repeated on each page that contains table data

А есть и указание о том, что тело таблицы можно скроллить, а шапка и подвал будут оставаться на месте:


Table rows may be grouped into a table head, table foot, and one or more table body sections, using the THEAD, TFOOT and TBODY elements, respectively. This division enables user agents to support scrolling of table bodies independently of the table head and foot

А по факту браузеры этого не делают и скролл для таблицы необходимо придумывать/настраивать вручную.


Есть много способов это сделать, но все они сводяться к тому, что:


  1. мы не создаем дополнительную разметку и пытаемся прикрутить скролл к тому что есть (к телу таблицы, или оборачиваем в контейнер, а значение ячеек в шапке делаем абсолютно позиционированным)

Можно задать ограниченную высоту телу таблицы. Следующий пример показывает, что можно попробовать задать высоту тела таблицы.
В результате мы ломаем табличное отображение тела таблицы CSS свойством display: block, и при этом необходимо синхронизировать прокрутку шапки с телом таблицы.


  1. мы создаём дополнительную разметку (составные таблицы) и тогда при прокрутке оригинала мы синхронизируем дополнительную разметку

Этот вариант, где все предлагают/строят решения.


Примеры составных таблиц


Если нам необходима прокрутка тела таблицы, то без составных разметок не обойтись. Все примеры составных таблиц используют свои пользовательские разметки.


Одна из самых известных таблиц Data Tables использует следующую разметку:


Data Tables HTML

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


Мы видим в разметке две таблицы, хотя для пользователя это "видится" как одна.
Следующий пример React Bootstrap Table, если посмотреть в разметку, использует тоже две таблицы:


React Bootstrap Table HTML

Верхняя таблица отображает шапку, нижняя — тело. Хотя для пользователя кажется как будто бы это одна таблица.


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


А как же так получается, что тело таблицы (одна таблица) и шапка (другая таблица) подстраиваются под ширину контейнера и они никак не разъезжаются по ширине и совпадают друг с другом?


Тут кто как умеет так и синхронизирует, например, вот функция синхронизации ширины из вышеприведенной библиотеки:


_adjustHeaderWidth
componentDidUpdate() {
  ...
  this._adjustHeaderWidth();
  ...
}

_adjustHeaderWidth() {
    ...
    // берем ширину столбцов из тела таблицы если есть хоть один ряд, или берем ширину  из тела таблицы
    // и присваиваем шапке полученные размеры
  }

Возникает вполне логичный вопрос, а зачем тогда вообще использовать тег

, если используется только автоподстройка ширины из стандартной таблицы?

И тут мы окажемся не первыми, некоторые вообще не используют табличную разметку. Например Fixed Data Table или React Table.


Разметка в примерах примерно такая:


Разметка

Отсюда название fixed table, т.е. для такой разметки мы должны заранее указать ширину всех столбцов (ширину таблицы, иногда и высоту строки). Хотя если мы хотим сокращение текста, все равно необходимо задавать ширину столбцов, даже в обычной таблице.


Следующая таблица Reactabular использует интересный подход в синхронизации.


Автор пошел дальше и сделал прокручиваемым не только тело, но и шапку таблицы. В браузерах которые показывают ползунок скролла — выглядит ужасно, зато в touch браузерах очень классно и функционально.


Если мы скроллим тело таблицы, то происходит синхронизация шапки, а если мы скроллим шапку, то происходит синхронизация тела.


А как же сделать автоподстройку ширины колонки в составной таблице спросите вы? Вот интересный способ использовать дополнительный проход браузера. Например в этой таблице ag Grid можно автоматически рассчитать подходящую ширину столбца.


В коде есть функция автоподстройки ширины колонки:


getPreferredWidthForColumn
public getPreferredWidthForColumn(column: Column): number {
  // создать 
  // добавить в него все ячейки столбца
  // вычислить ширину span (вычисляет браузер)
  // удаляем 
}

Реализация собственной таблицы


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


Все составные таблицы (и моя) страдают недостатком, у них нет стандарта как их кастомизировать/настраивать (и это логично, т.к. при реализации отказались от HTML4 таблицы).


Когда ты начинаешь изучать одну составную таблицу, потом начинаешь тратить время на её кастомизацию.


Затем для другого проекта изучаешь другую таблицу (например при переходе с Angular1 на React, или с jQuery на Vue), а кастомизация совсем другая.


Возникает логичный вопрос, а стоит ли потраченное время того? Стоит ли учить снова и снова связку фреймворк-таблица?


Может легче освоить для себя базовые моменты составной таблицы и тогда вы сможете делать свою таблицу на любом фреймворке (Angular/React/Vue/будущее...)? Например, на свою таблицу вы будете тратить 2 дня на старт, потом в течении 30 мин кастомизировать.


А можно подключить готовую фреймворк-таблицу за 30 мин и потом кастомизировать каждую фичу за 1 день.


К премеру, я покажу как сделать свою составную таблицу на React.


Таблица будет:


  • составной, синхронизировать шапку в зависимости от тела таблицы
  • подстраивать свою ширину если она меньше ширины контейнера

Дальше будет объяснение только некоторых аспектов разработки, можете сразу посмотреть результат.


Разметка


Для разметки будем использовать div элементы. Если использовать display: inline-block для ячеек, тогда будет следующая разметка:


Inline block HTML

Но есть одна проблема — браузер (не все браузеры) интерпретирует пустые места между ячейками как текстовые ноды.


Есть отличная статья, как с этим бороться.


И если мы используем шаблонизатор (EJS, JSX, Angular, Vue), то это легко решить:


inline block fixed HTML
{value}
{value}

Однако уже 2017 год, flexbox давно поддерживается, я делал на нем проекты еще в 2014 для IE11.


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


Flexbox HTML
{value}
{value}

Общие моменты использования


Таблица должна встраиваться в Redux архитектуру, примеры таких таблиц предалагают подключать свои reducers.


Мне этот подход не нравится. По моему мнению, разработчик должен контроллировать процесс сортировки, фильтрации. Это требует дополнительного кода.


Вместо такого "черного ящика", который потом сложно кастомизировать:


Обычное подключение
render() {
  return (
    
) }

разработчик должен будет писать:


Собственное подключение
render() {
  const 
    descriptions = getColumnDescriptions(this.getTableColumns()),
    filteredData = filterBy([], []),
    sortedData = sortBy(filteredData, []);
  return (
    
) }

Разработчик должен сам прописывать шаги: вычислить описание колонок, отфильтровать, отсортировать.


Все функции/конструкторы getColumnDescriptions, filterBy, sortBy, TableHeader, TableBody, TableColumn будут импортироваться из моей таблицы.


В качестве данных будет использоваться массив объектов:


Пример данных
[
  { "Company": "Alfreds Futterkiste", "Cost": "0.25632" },
  { "Company": "Francisco Chang", "Cost": "44.5347645745" },
  { "Company": "Ernst Handel", "Cost": "100.0" },
  { "Company": "Roland Mendel", "Cost": "0.456676" },
  { "Company": "Island Trading Island Trading Island Trading Island Trading Island Trading", "Cost": "0.5" },
]

Мне понравился подход создания описания колонок в jsx в качестве элементов.


Будем использовать ту же идею, однако, чтобы сделать независимыми шапку и тело таблицы, будем вычислять описание один раз и передавать его и в шапку и в тело:


Описание колонок и подключение
getTableColumns() {
  return [
    first header row,
    
      Company
    ,
    
      Cost
    ,
  ];
}

render() {
  const 
    descriptions = getColumnDescriptions(this.getTableColumns());
  return (
    
) }

В функции getTableColumns мы создаем описание колонок.


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


Обязательно указываем row — число, которое показывает индекс строки в шапке (если шапка будет группироваться).


Параметр dataField, определяет какой ключ из объекта использовать для получения значения.


Ширина width тоже обязательный параметр, может задаватся как числом или как массивом ключей от которых зависит ширина.


В примере верхняя строка в таблице row={0} зависит от ширины двух колонок ["Company", "Cost"].


Элемент TableColumn "фейковый", он никогда не будет отображаться, а вот его содержимое this.props.children — отображается в ячейке шапки.


Разработка


На основе описаний колонок сделаем функцию, которая будет разбивать описания по рядам и по ключам, а также будет сортировать описания по рядам в результирующем массиве:


getColumnDescriptions
function getColumnDescriptions(children) {
  let byRows = {}, byDataField = {};
  React.Children.forEach(children, (column) => {
    const {row, hidden, dataField} = column.props;
    if (column === null || column === undefined || typeof row !== 'number' || hidden) { return; }
    if (!byRows[row]) { byRows[row] = [] }
    byRows[row].push(column);
    if (dataField) { byDataField[dataField] = column }
  });
  let descriptions = Object.keys(byRows).sort().map(row => {
    byRows[row].key = row;
    return byRows[row];
  });
  descriptions.byRows = byRows;
  descriptions.byDataField = byDataField;
  return descriptions;
}

Теперь обработанные описания передаём в шапку и в тело для отображения ячеек. Шапка будет строить ячейки так:


Header render
getFloor(width, factor) {
  return Math.floor(width * factor);
}

renderChildren(descriptions) {
  const {widthFactor} = this.props;
  return descriptions.map(rowDescription => {
    return 
{rowDescription.map((cellDescription, index) => { const {props} = cellDescription; const {width, dataField} = props; const _width = Array.isArray(width) ? width.reduce((total, next) => { total += this.getFloor(descriptions.byDataField[next].props.width, widthFactor); return total; }, 0) : this.getFloor(width, widthFactor); return
{cellDescription.props.children}
})}
}) } render() { const {className, descriptions} = this.props; return (
{this.renderChildren(descriptions)}
) }

Тело таблицы будет строить ячейки тоже на основе обработанных описаний колонок:


Body render
renderDivRows(cellDescriptions, data, keyField) {
  const {rowClassName, widthFactor} = this.props;
  return data.map((row, index) => {
    return 
{cellDescriptions.map(cellDescription => { const {props} = cellDescription; const {dataField, dataFormat, cellClassName, width} = props; const value = row[dataField]; const resultValue = dataFormat ? dataFormat(value, row) : value; return
{resultValue ? resultValue : '\u00A0'}
})}
}); } getCellDescriptions(descriptions) { let cellDescriptions = []; descriptions.forEach(rowDescription => { rowDescription.forEach((cellDescription) => { if (cellDescription.props.dataField) { cellDescriptions.push(cellDescription); } }) }); return cellDescriptions; } render() { const {className, descriptions, data, keyField} = this.props; const cellDescriptions = this.getCellDescriptions(descriptions); return (
{this.renderDivRows(cellDescriptions, data, keyField)}
) }

Тело таблицы использует описания у которых есть свойство dataField, поэтому описания фильтруются используя функцию getCellDescriptions.


Тело таблицы будет слушать события изменения размеров экрана, а также прокрутки самого тела таблицы:


Слушатели
componentDidMount() {
  this.adjustBody();
  window.addEventListener('resize', this.adjustBody);
  if (this.tb) {
    this.tb.addEventListener('scroll', this.adjustScroll);
  }
}

componentWillUnmount() {
  window.removeEventListener('resize', this.adjustBody);
  if (this.tb) {
    this.tb.removeEventListener('scroll', this.adjustScroll);
  }
}

Подстройка ширины таблицы происходит следующим образом.


После отображения берётся ширина контейнера, сравнивается с шириной всех ячеек, если ширина контейнера больше, увеличивается ширина всех ячеек.


Для этого разработчик должен хранить состояние коэффициента ширины (который будет меняться).


Следующие функции реализованы в таблице, однако разработчик может использовать свои. Чтобы использовать уже реализованные, необходимо их импортировать и прилинковать к текущему компоненту:


Линковка
constructor(props, context) {
  super(props, context);

  this.state = {
    activeSorts: [],
    activeFilters: [],
    columnsWidth: {
      Company: 300, Cost: 300
    },
    widthFactor: 1
  };

  this.handleFiltersChange = handleFiltersChange.bind(this);
  this.handleSortsChange = handleSortsChange.bind(this);
  this.handleAdjustBody = handleAdjustBody.bind(this);
  this.getHeaderRef = getHeaderRef.bind(this, 'th');
  this.getBodyRef = getBodyRef.bind(this, 'tb');
  this.syncHeaderScroll = syncScroll.bind(this, 'th');
}

Функция подстройки ширины:


adjustBody
adjustBody() {
  const {descriptions, handleAdjustBody} = this.props;
  if (handleAdjustBody) {
    const cellDescriptions = this.getCellDescriptions(descriptions);
    let initialCellsWidth = 0;
    cellDescriptions.forEach(cd => {
      initialCellsWidth += cd.props.width;
    });
    handleAdjustBody(this.tb.offsetWidth, initialCellsWidth);
  }
}

Функция синхронизация шапки:


adjustScroll
adjustScroll(e) {
  const {handleAdjustScroll} = this.props;
  if (typeof handleAdjustScroll === 'function') {
    handleAdjustScroll(e);
  }
}

Ключевая особенность таблицы для redux — это то, что она не имеет своего внутреннего состояния (она должна иметь состояние, но только в том месте, где укажет разработчик).


И подстройка ширины adjustBody и синхронизация скролла adjustScroll — это функции которые изменяют состояние у прилинкованного компонента.


Внутрь TableColumn можно вставлять любую jsx разметку. Зачастую используются такие варианты: текст, кнопка сортировки и кнопка фильтрации.


Для массива активных сортировок/фильтраций разработчик должен создать состояние и передавать его в таблицу.


Состояние
this.state = {
  activeSorts: [],
  activeFilters: [],
};

Передаем в таблицу массив активных сортировок/фильтраций:


getTableColumns
getTableColumns() {
  const {activeFilters, activeSorts, columnsWidth} = this.state;
  return [
    first header row,
    
      
    ,
    
      
    ,
  ];
}

Компонент сортировки SortButton и компонент фильтрации MultiselectDropdown при изменении "выбрасывают" новые активные фильтры/сортировки, которые разработчик должен заменить в состоянии. Массивы activeSorts и activeFilters как раз и предполагают, что возможна множественная сортировка и множественная фильтрация по каждой колонке.


К сожалению, формат статьи не позволяет описать всех тонкостей, поэтому предлагаю сразу посмотреть результат.


Итого разработчику в таблице необходимо:


  • автоподстройка ширины таблицы под ширину контейнера
  • прокрутка тела таблицы и синхронизация шапки
  • сортировка таблицы (возможна множественная сортировка)
  • фильтрация таблицы (возможна множественная фильтрация)

Все это я реализовал в примере. Надеюсь теперь, при переходе на новый фреймворк, у вас как минимум появился выбор — брать готовую или сделать свою таблицу.


Исходники находятся здесь.

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331494/


Метки:  

Чипы Intel Skylake и Kaby Lake — обнаружена проблема при активном Hyper-Threading

Понедельник, 26 Июня 2017 г. 17:48 + в цитатник
В мае — апреле этого года Intel обновляла документацию на свои процессоры. Стало известно почему — появилось описание новой ошибки. Согласно документу, опубликованному Debian, чипы с микроархитектурой Skylake и Kaby Lake, а также серверные процессоры Xeon v5 и v6 и некоторые процессоры Pentium могут вести себя непредсказуемо при активном Hyper-Threading.

/ фото Ultra Mendoza PD

В документации Intel ошибка описывается следующим образом: «Короткие циклы из менее чем 64 инструкций, использующих регистры AH, BH, CH или DH, а также соответствующие им регистры большей разрядности (например, RAX, EAX или AX для AH), могут вызывать непредсказуемое поведение системы. Проблема наблюдается только в том случае, если активны оба логических процессора на одном физическом процессоре».


При этом в сообществе нашлись «пострадавшие». Еще в начале этого года с проблемой столкнулся разработчик инструмента OCaml, который наблюдал странное поведение компилятора. Локализовать ошибку он смог только сейчас, когда её описание появилось в документации Intel.

Согласно данным Debian, под «непредсказуемым поведением системы» понимается широкий спектр проблем: от неправильной работы приложений до повреждения и потери данных. Поэтому участники проекта призывают владельцев компьютеров на базе процессоров с микроархитектурами Skylake и Kaby Lake отключить Hyper-Threading в BIOS или UEFI. При этом подчёркивается, что проблема касается не только Debian или Linux, и может проявляться в любых операционных системах, включая Windows.

Поскольку Intel знают о проблеме, в скором времени она должна быть устранена. Микрокод с коррекцией ошибки на данный момент выпущен лишь для процессоров со Skylake (0xB9, 0xBA и позднее). Кроме того, в новых процессорах на Kaby Lake-X ошибка исправлена изначально в степпинге ядра B0. Конечные пользователи получат необходимый патч через обновления BIOS материнских плат.

Вся эта ситуация породила довольно бурное обсуждение в комментариях к новости. Один из резидентов платформы Hacker News отметил, что в связи с частыми появлениями багов в процессорах, производители должны чаще проводить регрессивное тестирование и другие виды испытаний, даже связанные с применением вредоносного ПО, если это позволит исключить подобные ситуации. При этом пользователи HN назвали баги такого рода крайне неприятными.

«Когда ты работаешь с CPU и памятью, то всегда считаешь, что ты ошибся в коде. Думаешь, что в этом причина, — делится один из разработчиков. — Поэтому, когда «натыкаешься» на аппаратную проблему, это сводит тебя с ума».

Отметим, что обнаруженная ошибка — уже не первая «неприятность» подобного характера, поразившая микроархитектуру Skylake. В начале прошлого года в ней была выявлена ещё одна критичная для конечных пользователей проблема, приводившая к зависаниям и сбоям процессора под высокой нагрузкой.

P.S. Еще несколько материалов из нашего блога:


P.P.S. В нашем блоге на Хабре: 100 практических материалов по безопасности, экономике и инструментарию IaaS.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/330580/


Метки:  

Комплексный подход по защите от направленных атак и вымогательского ПО типа Ransomware

Понедельник, 26 Июня 2017 г. 17:28 + в цитатник

Аннотация


Последние несколько лет на рынке информационной безопасности остро встал вопрос защиты от автоматизированных направленных атак, однако в общем понимании направленная атака в первое время представлялась как результат продолжительной и профессиональной работы организованной группой киберпреступников с целью получения дорогостоящих критичных данных. В настоящее время на фоне развития технологий, популяризации open-source форумов (напр. Github, Reddit) и Darknet, предоставляющих исходные коды вредоносного ПО и пошагово описывающих действия по его модификации (для невозможности его детектирования сигнатурным анализом) и заражению хостов, реализация кибератак значительно упростилась. Для реализации успешной атаки, сопровождающейся пагубными последствиями для владельцев автоматизированных и информационных систем, достаточно неквалифицированного пользователя и энтузиазма в разборе предоставленного в Интернет / Darknet материала.
Мотивом для осуществления подобной преступной деятельности является получение прибыли, самым простым, и поэтому самым распространенным, способом является заражение сетевых хостов вредоносным ПО типа Ransomware. За последние 2 года его популярность стремительно растет:
  • за 2016 год количество известных типов (семейств) троянов-вымогателей увеличилось на 752%: с 29 типов в 2015 году до 247 к концу 2016 года (по данным TrendLabs );
  • благодаря вирусам-вымогателям злоумышленники за 2016 год «заработали» 1 миллиард долларов США (по данным CSO );
  • в 1 квартале 2017 года появилось 11 новых семейств троянов-вымогателей и 55 679 модификаций. Для сравнения, во 2-4 кварталах 2016 года появилось 70 837 модификаций (по данным Kaspersky Lab ).

В начале 2017 года ведущие производители средств защиты информации (Kaspersky Lab, McAfee Labs, SophosLabs, Malwarebytes Labs, TrendMicro и др.) называли Ransomware одной из основных угроз безопасности информации для государственных и коммерческих организаций различных сфер деятельности и масштабов, и как показывает история, они не ошиблись:
  • Январь 2017 г. Заражение 70% камер видеонаблюдения за общественным порядком в Вашингтоне накануне инаугурации президента. Для устранения последствий камеры были демонтированы, перепрошиты или заменены на другие;
  • Февраль 2017 г. Вывод из строя всех муниципальных служб округа Огайо, США, более чем на 1 неделю из-за массового шифрования данных на серверах и рабочих станциях пользователей (более 1000 хостов);
  • Март 2017 г. Вывод из строя систем Капитолия штата Пенсильвания, США, из-за атаки и блокировки доступа к данным информационных систем;
  • Май 2017 г. Крупномасштабная атака вируса-шифровальщика WannaCry (WanaCrypt0r 2.0), поразившая на 26.06.2017 более 546 тысяч компьютеров и серверов на базе операционных систем семейства Windows в более чем 150 странах. В России были заражены компьютеры и серверы таких крупных компаний, как Минздрав, МЧС, РЖД, МВД, «Мегафон», «Сбербанк», «Банк России». Универсального дешифратора данных до сих пор не существует (были опубликованы способы расшифровать данные на Windows XP), общий ущерб от вируса по оценкам экспертов превышает 1 млрд долларов США;
  • Крупномасштабная атака вируса-шифровальщика XData в мае 2017 года (через неделю после начала атаки WannaCry), использующая для заражения аналогичную WannaCry уязвимость (EternalBlue) в протоколе SMBv1, поразившая в основном корпоративный сегмент Украины (96% зараженных компьютеров и серверов находятся на территории Украины), скорость распространения которого превышает WannaCry в 4 раза. В настоящий момент ключ шифрования опубликован, выпущены дешифраторы для жертв вымогателя;
  • Июнь 2017 г. Обширной атаке Ransomware была подвержена сеть одного из крупнейших университетов мира – Univercity College London, атака была направлена на блокирование доступа к общим сетевым хранилищам, автоматизированную систему студенческого управления, выполнено это было в предэкзаменационный и выпускной период, когда студенты, хранящие свои дипломные работы на файловых серверах университета, вероятнее всего заплатят мошенникам с целью получения своей работы. Объем зашифрованных данных и пострадавших не раскрывается.

Случаев направленных атак с целью заражения Ransomware очень много, основной целью злоумышленников являются системы на базе ОС семейства Windows, однако существуют различные версии Ransomware для ОС семейств UNIX/Linux, MacOS, а также мобильных платформ iOS и Android.
С развитием Ransomware появляются и средства противодействия им – в первую очередь это открытый проект No more Ransom! (www.nomoreransom.org), предоставляющий жертвам атак средства дешифрования данных (в случае вскрытия ключа шифрования), во вторую – специализированные open-source средства защиты от вирусов-шифровальщиков, но и они либо анализируют поведение ПО по сигнатурам и не способны обнаружить неизвестный вирус, либо обеспечивают блокировку вредоносного ПО после его воздействия на систему (шифрования части данных). Специализированные Open-source решения применимы пользователями-интернет на личных / домашних устройствах, крупным организациям, обрабатывающим большие объемы информации, в том числе критичной, необходимо обеспечивать комплексную проактивную защиту от направленных атак.

Проактивная защита от направленных атак и Ransomware


Рассмотрим возможные векторы доступа к защищаемой информации, находящейся на сервере или автоматизированном рабочем месте пользователя:
  • Воздействие на периметр локальной вычислительной сети из интернета возможно через:
  • корпоративную электронную почту;
  • веб-трафик, в том числе веб-почту;
  • периметровый маршрутизатор / межсетевой экран;
  • сторонние (некорпоративные) шлюзы доступа к интернету (модемы, смартфоны и т. д.);
  • системы защищенного удаленного доступа.
  • Воздействие на серверы, рабочие места пользователей по сети:
  • загрузка вредоносных программ на конечные точки / серверы по запросу от них же;
  • использование недокументированных возможностей (уязвимостей) системного/прикладного ПО;
  • загрузка вредоносов по шифрованному VPN-каналу, неконтролируемому службами ИТ и ИБ;
  • подключение к локальной сети нелегитимных устройств.
  • Прямое воздействие на информацию на серверах, рабочих местах пользователей:
  • подключение внешних носителей информации с вредоносом;
  • разработка вредоносных программ прямо на конечной точке / сервере.

Для уменьшения вероятности реализации угрозы для каждого типа доступа к защищаемой информации необходимо обеспечивать выполнение комплекса организационно-технических мер по защите информации, перечень которых отражен на рисунке (см. Рисунок 1)

Рисунок 1. Проактивные меры защиты от направленных атак и Ransomware

Организационные меры защиты от направленных атак и Ransomware


К основным организационным мерам проактивной защиты от направленных атак и Ransomware относятся:
  • Повышение осведомленности сотрудников в области ИБ.
    Необходимо регулярно проводить обучение сотрудников и информировать их о возможных угрозах ИБ. Минимальной и необходимой мерой является формирование принципов работы с файлами и почтой:
    o не открывать файлы с двойным расширением: настроить для пользователей отображение расширений, чтобы идентифицировать вредоносные файлы с двойными расширениями (например, 1СRecord.xlsx.scr);
    o не включать макросы в недоверенных документах Microsoft Office;
    o проверять адреса отправителей почтовых сообщений;
    o не открывать ссылки на веб-страницы, почтовые вложения от неизвестных отправителей.
  • Оценка эффективности защиты как внутри организации, так и с привлечением внешних специалистов.
    Оценивать эффективность обучения персонала необходимо при помощи моделирования атак, как внутренних, так и с участием внешних специалистов — проводить тесты на проникновение, в т. ч. с использованием метода социальной инженерии.
  • Регулярное обновление системного ПО (Patch Management).
    Для предотвращения атак вредоносного ПО на целевые системы через известные уязвимости необходимо обеспечить своевременное тестирование и установку обновлений системного и прикладного ПО с учетом приоритизации по степени критичности обновлений.
  • Систематизация резервного копирования данных.
    Необходимо регулярно выполнять резервное копирование критически важных данных серверов информационных систем, систем хранения данных, рабочих мест пользователей (если предполагается хранение критичной информации). Резервные копии должны храниться на ленточных библиотеках системы хранения данных, на отчуждаемых носителях информации (при условии, что носитель информации не подключен постоянно к рабочей станции или серверу), а также в облачных системах резервирования данных, хранилищах.

Технические меры защиты от направленных атак и Ransomware


Технические мероприятия проактивной защиты от направленных атак и Ransomware предпринимаются на уровне сети и на уровне хоста.

Меры проактивной защиты на уровне сети


  • Использование систем фильтрации электронной почты, обеспечивающих анализ почтового трафика на наличие нежелательных писем (spam), ссылок, вложений, в том числе вредоносных (например, блокировка файлов JavaScript (JS) и Visual Basic (VBS), исполняемые файлы (.exe), файлы заставки (SCR), Android Package (.apk) и файлы ярлыков Windows (.lnk)).
  • Использование систем контентной фильтрации веб-трафика, обеспечивающих разграничение и контроль доступа пользователей к интернету (в т. ч. путем разбора SSL-трафика с помощью подмены сертификата сервера), потоковый анализ трафика на наличие вредоносных программ, разграничение доступа пользователей к содержимому веб-страниц.
  • Использование систем защиты от целенаправленных атак, атак нулевого дня (Sandbox, песочница), обеспечивающих эвристический и поведенческий анализ потенциально опасных файлов в изолированной среде перед отправкой файла в защищаемые информационные системы. Системы защиты от направленных атак должны быть интегрированы с системами контентной фильтрации веб-трафика, фильтрации электронной почты для блокирования вредоносных вложений. Также системы защиты от направленных атак интегрируют с информационными системами внутри периметра сети для обнаружения и блокировки сложных атак на критичные ресурсы, сервисы.
  • Обеспечение контроля доступа к корпоративной сети на уровне проводной и беспроводной сети с помощью технологии 802.1x. Такая мера исключает несанкционированное подключение нелегитимных устройств в корпоративную сеть, обеспечивает возможность выполнения проверки на соответствие корпоративным политикам при доступе в сеть организации (наличие антивирусного ПО, актуальные сигнатурные базы, наличие критических обновлений Windows). Контроль доступа к корпоративной сети с помощью 802.1x обеспечивается системами класса NAC (Network Access Control).
  • Исключение прямого взаимодействия внешних пользователей с ресурсами корпоративных информационных систем с помощью промежуточных шлюзов доступа с наложенными корпоративными средствами защиты информации (терминальный сервер, система виртуализации рабочих столов VDI), в том числе с возможностью фиксации действий внешних пользователей с помощью видео или текстовой записи сессии. Мера реализуется с помощью систем терминального доступа, систем класса PUM (Privileged User Management).
  • Сегментирование сети по принципу необходимой достаточности для исключения избыточных разрешений сетевого взаимодействия, ограничения возможности распространения вредоносных программ в корпоративной сети в случае заражения одного из серверов / рабочих мест пользователей / виртуальных машин. Возможна реализация такой меры с помощью систем анализа политик межсетевого экранирования (NCM / NCCM, Network Configuration (Change) Management), обеспечивающих централизованный сбор политик межсетевого экранирования, настроек межсетевых экранов и дальнейшую их обработку с целью автоматизированной выдачи рекомендаций по их оптимизации, контроль изменений политик межсетевого экранирования.
  • Выявление аномалий на уровне сетевых взаимодействий с помощью специализированных решений класса NBA & NBAD (Network Behavior Analysis, Network Behavior Anomaly Detection), позволяющих осуществить сбор и анализ сведений о потоках данных, профилирование трафика для каждого сетевого хоста для выявления отклонений от «нормального» профиля. Данный класс решений позволит выявить:
    o сканирование зараженным хостом своего окружения;
    o вектор заражения;
    o состояние хоста — «просканирован», «заражен и сканирует других»;
    o однонаправленные потоки;
    o аномальные потоки;
    o вирусные эпидемии;
    o распределенные атаки;
    o картину существующих потоков.
  • Отключение зараженных хостов (автоматизированных рабочих мест, серверов, виртуальных машин и пр.) от сети. Эта мера применима в случае заражения хотя бы одного из хостов в корпоративной сети, однако необходима для локализации и предотвращения вирусной эпидемии. Рабочие места от сети можно отключить как силами администрирующего персонала ИТ и ИБ, так и автоматизировано при обнаружении признаков угрозы на защищаемом хосте (путем корреляции событий безопасности, настройки автоматизированных действий по блокировки всех сетевых активностей на хосте / отключению хоста от сети на уровне коммутатора и пр.).

Меры проактивной защиты на уровне хоста


  • Обеспечение защиты от несанкционированного доступа рабочих мест, серверов, виртуальных машин путем усиленной аутентификации пользователей, контроля целостности операционной системы, блокировки загрузки системы с внешних носителей для исключения заражения корпоративной сети нарушителями внутри периметра сети. Эта мера реализуется решениями класса СЗИ от НСД / Endpoint Protection.
  • Обеспечение антивирусной защиты на всех сетевых узлах организации. Антивирусное ПО должно обнаруживать факты вирусного заражения оперативной памяти, локальных носителей информации, томов, каталогов, файлов, а также файлов, получаемых по каналам связи, электронных сообщений на рабочих местах, серверах, виртуальных машинах в реальном времени, лечить, удалять или изолировать угрозы. Сигнатурные базы антивирусного ПО должны регулярно обновляться и находиться в актуальном состоянии.
  • Обеспечение мониторинга и контроля действий ПО на защищаемых хостах путем контроля запускаемых служб и сервисов, эвристического анализа их функционирования. Такая мера реализуется решениями класса HIPS (Host Intrusion Prevention).
  • Обеспечение контроля подключения внешних устройств, блокировки неиспользуемых портов на защищаемых хостах для исключения подключения к защищаемым хостам несанкционированных устройств: как носителей информации с потенциально вредоносными программами, так и внешних шлюзов доступа к интернету (например, 4G-модем), обеспечивающих неконтролируемый и незащищенный канал доступа в интернет. Эта мера реализуется решениями класса СЗИ от НСД / Endpoint Protection.
  • Обеспечение продвинутой защиты хостов с помощью поведенческого анализа функционирования процессов на защищаемых хостах, машинного обучения, эвристического анализа файлов, контроля приложений, защиты от эксплойтов для выявления и блокировки неизвестных угроз (угроз нулевого дня) в режиме реального времени. Данная мера реализуется решениями класса NGEPP (Next Generation Endpoint Protection).
  • Использование агентских решений по защите от вымогателей, шифрующих данные на зараженном хосте. К ним относятся:
    o Продуктивные системы защиты от направленных атак, атак нулевого дня с клиент-серверной архитектурой. Клиентское ПО устанавливается на защищаемый хост, защищает в реальном времени от угроз нулевого дня, вирусов, шифрующих данные в системе, расшифровывает зашифрованные вредоносом данные (в случае наличия агента — до попытки заражения), удаляет троян-вымогатель, защищает от фишинговых атак. Клиентское ПО обеспечивает контроль всех каналов доступа к хосту: веб-трафик, отчуждаемые носители информации, электронная почта, доступ по локальной сети, вредоносные программы в зашифрованном трафике (VPN).
    o Клиентские системы защиты от угроз нулевого дня (песочницы) в открытом доступе (sandboxie, cuckoo sandbox, shadow defender и др.).
    o Клиентские системы защиты от угроз нулевого дня на базе микровиртуализации (Bromium vSentry), обеспечивающие поведенческий анализ потенциально вредоносных файлов в аппаратно изолированной среде (микровиртуальной инфраструктуре).
  • Обеспечение межсетевого экранирования на уровне хоста с помощью программных межсетевых экранов для разграничения доступа к ресурсам корпоративной сети, ограничения распространения вредоноса в случае заражения хоста, блокировки неиспользуемых сетевых портов, протоколов.

Другие меры защиты от вирусов-вымогателей


Дополнительно к вышеперечисленным мерам предотвратить направленную атаку в корпоративной сети поможет следующее:
  • Обеспечение регулярного анализа защищенности ИТ-инфраструктуры — сканирование узлов сети для поиска известных уязвимостей в системном и прикладном ПО. Эта мера обеспечивает своевременное обнаружение уязвимостей, позволяет их устранить до момента их использования злоумышленниками. Также система анализа защищенности решает задачи по контролю сетевых устройств и устройств, подключенных к рабочим станциям пользователей (например, 4G-модем).
  • Сбор и корреляция событий позволяет комплексно подойти к обнаружению вымогателей в сети на основе SIEM-систем, поскольку такой метод обеспечивает целостную картину ИТ-инфраструктуры компании. Эффективность SIEM заключается в обработке событий, которые отправляются с различных компонентов инфраструктуры, в том числе ИБ, на основе правил корреляции, что позволяет оперативно выявить потенциальные инциденты, связанные с распространением вируса-вымогателя.

Приоритезация мер защиты от вирусов-вымогателей


Надежная комплексная защита от направленных атак обеспечивается комплексом организационно-технических мер, которые ранжируются в следующие группы:
  • Базовый набор мер, необходимый для применения всем организациям для защиты от направленных атак и вредоносов-вымогателей.
  • Расширенный набор мер, применимый для средних и крупных организаций с высокой стоимостью обработки информации.
  • Продвинутый набор мер, применимый для средних и крупных организаций с продвинутой ИТ- и ИБ-инфраструктурой и высокой стоимостью обрабатываемой информации.



Рисунок 2. Приоритизация мер защиты от трояна-вымогателя

Меры защиты от Ransomware для конечных пользователей


Угроза заражения вирусом-вымогателем актуальна и для конечных пользователей Интернет, для которых также применимы отдельные меры по предотвращению заражения:
  • своевременная установка обновлений системного ПО;
  • использование антивирусов;
  • своевременное обновление баз сигнатур антивирусов;
  • использование доступных в свободном доступе средств защиты от вредоносных программ, шифрующих данные на компьютере: RansomFree, CryptoDrop, AntiRansomware tool for business, Cryptostalker и др. Установка средств защиты данного класса применима, если на компьютере хранятся критичные незарезервированные данные и не установлены надежные средства антивирусной защиты.

Уязвимость мобильных устройств (Android, iOS)


«Умные» мобильные устройства (смартфоны, планшетные компьютеры) стали неотъемлемой частью жизни: с каждым годом увеличивается количество активированных мобильных устройств, мобильных приложений и объем мобильного трафика. Если раньше мобильные телефоны хранили только базу контактов, то сейчас они являются хранилищами критичных данных для пользователя: фото, видео, календари, документы и пр. Мобильные устройства все активнее используются и в корпоративном секторе (ежегодный прирост 20-30%). А потому растет интерес злоумышленников и к мобильным платформам, в частности, с точки зрения вымогания денег с помощью троянов. По данным Kaspersky Lab, в 1 квартале 2017 года вымогатели занимают 16% от общего числа вредоносов (в 4 квартале 2016 года это значение не превышало 5%). Наибольший процент троянов для мобильных платформ написан для самой популярной мобильной операционной системы — Android, но для iOS также существуют подобные.
Меры защиты для мобильных устройств:
  • Для корпоративного сектора:
    o использование систем класса Mobile Device Management (MDM), обеспечивающих контроль установки обновлений системного ПО, установки приложений, контроль наличия прав суперпользователя;
    o для защиты корпоративных данных на мобильных устройствах пользователя — системы класса Mobile Information Management (MIM), обеспечивающих хранение корпоративных данных в зашифрованном контейнере, изолированном от операционной системы мобильного устройства;
    o использование систем класса Mobile Threat Prevention, обеспечивающих контроль разрешений, предоставленных приложениям, поведенческий анализ мобильных приложений.
  • Для конечных пользователей:
    o использование официальных магазинов для установки приложений;
    o своевременное обновление системного ПО;
    o исключение перехода по недоверенным ресурсам, установки недоверенных приложений и сервисов.

Выводы


Простота реализации и низкая стоимость затрат организации кибератак (Ransomware, DDoS, атаки на веб-приложения и пр.) приводит к увеличению числа киберпреступников при одновременном снижении среднего уровня технической осведомленности атакующего, в связи с этим резко увеличивается вероятность реализации угроз безопасности информации в корпоративном секторе и потребность в обеспечении комплексной защиты.
Поэтому мы в компании «Информзащита» фокусируемся на современных вызовах информационной безопасности и обеспечиваем защиту инфраструктуры клиентов от новейших, в том числе неизвестных, угроз. Создавая и реализуя комплексные адаптивные модели противодействия угрозам информационной безопасности, мы знаем, как прогнозировать, предотвращать, обнаруживать и реагировать на киберугрозы. Главное — делать это своевременно.
Автор: Евгений Бородулин, главный архитектор компании «Информзащита»
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331700/


Интеграция инструментов статистического анализа кода для OpenStack на Jenkins CI

Понедельник, 26 Июня 2017 г. 17:06 + в цитатник
Интеграция PyLint с Jenkins

Установка pylint на Centos
pip install pylint
Генерация конфигурационного файла
touch pylint.cfg
pylint --generate-rcfile > pylint.cfg


После генерации файла его необходимо поместить в корневой каталог проекта

Запуск в Jenkins
1. В настройках Jenkins — управление плагинами установить плагины “Violations plugin” для генерации наглядного отчета
2. Создать проект (job) со свободной конфигурацией
3. В пункте управления исходным кодом вставить ссылку до репозитория
4. В пункте сборки выбрать “Выполнить команду shell” или “Execute Shell”. В поле ввода ввести команду:
find /var/lib/jenkins/workspace/$JOB_NAME/to/check> -iname "*.py" | xargs pylint --disable=all
5. В пункте послесборочных операций выбрать “Report Violations“
6. Сохранить проект и выполнить

Рис.1. Результат выполнения проверок PyLint
image
Рис.2. Результат выполнения проверок PyLint — продолжение 1
image
Рис.3. Результат выполнения проверок PyLint — продолжение 2
image
Рис.4. Результат выполнения проверок PyLint — продолжение 3
image
Рис.5. Результат выполнения проверок PyLint — продолжение 4
image
Рис.6. Результат выполнения проверок PyLint — продолжение 5
image

Интеграция SonarQube с Jenkins

Установка и настройка MySQL для SonarQube:
wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm
sudo rpm -ivh mysql-community-release-el7-5.noarch.rpm
sudo yum update -y

sudo yum install mysql-server

sudo systemctl start mysqld

sudo mysql_secure_installation - нажать enter если пароль по умолчанию отсутствует

mysql -u root -p

CREATE DATABASE sonar CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE USER 'sonar' IDENTIFIED BY 'sonar';
GRANT ALL ON sonar.* TO 'sonar'@'%' IDENTIFIED BY 'sonar';
GRANT ALL ON sonar.* TO 'sonar'@'localhost' IDENTIFIED BY 'sonar';


Установка SonarQube на Centos:
Скачивание установочного файла в /opt
cd /opt
sudo wget https://sonarsource.bintray.com/Distribution/sonarqube/sonarqube-6.0.zip

Установка unzip и java
sudo yum install unzip -y
sudo yum install java-1.8.0-openjdk -y

Разархивирование sonarqube
sudo unzip sonarqube-6.0.zip
mv sonarqube-6.0 sonarqube

Настройка конфигурационного файла
vi /opt/sonarqube/conf/sonar.properties

sonar.jdbc.username=sonar
sonar.jdbc.password=sonar
sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance

sonar.web.host=localhost
sonar.web.context=/sonar
sonar.web.port=9000
По ссылке http://localhost:9000 будет находиться сервер sonarqube


Запуск SonarQube:
cd /opt/sonar/bin/linux-x86-64/
sudo ./sonar.sh start

Настройка SonarQube как сервиса:
Создать файл
/etc/init.d/sonar
Скопировать в содержимое файла
#!/bin/sh
#
# rc file for SonarQube
#
# chkconfig: 345 96 10
# description: SonarQube system (www.sonarsource.org)
#
### BEGIN INIT INFO
# Provides: sonar
# Required-Start: $network
# Required-Stop: $network
# Default-Start: 3 4 5
# Default-Stop: 0 1 2 6
# Short-Description: SonarQube system (www.sonarsource.org)
# Description: SonarQube system (www.sonarsource.org)
### END INIT INFO

/usr/bin/sonar $*

Создание ссылки на SonarQube
sudo ln -s /opt/sonarqube/bin/linux-x86-64/sonar.sh /usr/bin/sonar
Настройка прав и добавление в boot
sudo chmod 755 /etc/init.d/sonar
sudo chkconfig --add sonar

Запуск sonar
sudo service sonar start
Установка sonar runner:
yum install sonar-runner
wget http://repo1.maven.org/maven2/org/codehaus/sonar/runner/sonar-runner-dist/2.4/sonar-runner-dist-2.4.zip
unzip sonar-runner-dist-2.4.zip
mv sonar-runner-2.4 /opt/sonar-runner
export SONAR_RUNNER_HOME=/opt/sonar-runner
export PATH=$PATH:$SONAR_RUNNER_HOME/bin


Необходимо в корне проекта создать файл sonar-project.properties. Пример содержания файла
image

Запуск в Jenkins:
1. В настройках Jenkins - управление плагинами установить плагины “SonarQube Scanner for Jenkins” для генерации наглядного отчета
2. Перейти в “Настройки Jenkins” - “Конфигурирование системы” - “SonarQube servers” (если данной конфигурации не будет, то необходимо перезагрузить Jenkins). Далее нужно заполнить поля конфигурации:

Рис.8. Настройка SonarQube сервера в системе непрерывной интеграции Jenkins
image

3. Перейти в “Настройки Jenkins” - “Global Tool Configuration” - “SonarQube Scanner”. Заполнить поля конфигурации

Рис.9. Настройка SonarQube сканера в системе непрерывной интеграции Jenkins
image
4. Создать проект (job) со свободной конфигурацией
5. В пункте управления исходным кодом вставить ссылку до репозитория
6. В пункте сборки выбрать “Execute SonarQube Scanner”
В поле “Analysis properties” ввести параметры из файла sonar-project.properties или указать путь до данного файла
7. Сохранить проект и запустить
Рис.10. Результат выполнения работы SonarQube
image
Рис.11. Результат выполнения работы SonarQube - продолжение 1
image

Перейдя по ссылке из результата выполнения проекта localhost:9000/sonar/dashboard/index/keystone можно детально рассмотреть отчет по проверке качества кода keystone

Рис.12. Пример сгенерированного отчета SonarQube
image
Рис.13. Пример сгенерированного отчета SonarQube - продолжение 1
image
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331698/


Метки:  

«Когда управление идёт не сверху, а появляется внутри тебя»: о JS-разработке в SEMrush

Понедельник, 26 Июня 2017 г. 17:05 + в цитатник


На открытии нашей конференции HolyJS от компании SEMrush на сцену выходила фронтенд-разработчик Анастасия Манзюк, и в своём приветственном слове упомянула удивительно высокий уровень автономии внутри компании: каждая команда разработки самостоятельно принимает многие решения, которые в другой организации могут быть «спущены сверху».

Нам стало интересно, как это сказывается на разработке, и мы задали Анастасии несколько вопросов и об этом, и в целом о её фронтенд-работе в компании.



— Над чем лично вы работаете в SEMrush?

— Я из инфраструктурной команды, отвечающей за разработку различных внутренних сервисов, например, сервиса по работе с данными пользователей.

Нашими заказчиками являются другие команды нашей компании (разработчики или Sales). Для них мы делаем инструменты, обеспечивающие автоматизацию их работы.
Часто мы работаем не только на тёмной стороне SEMrush, но и выходим на свет: занимаемся различными улучшениями, которые видит пользователь. Они связаны с его профилем, авторизацией или регистрацией.

Также мы помогаем ещё 27 командам разработки в решении каких-то глобальных задач, но это уже немного другая история.

Но стоит отметить, что чаще в нашей компании можно столкнуться с представителем команды разработки, чей сервис создаётся для наших внешних пользователей — маркетологов и не только.

— А какая специфика у работы на «тёмной стороне», каково это — разрабатывать для своих?

— Думаю, что самое главное отличие — скорость получения фидбека. Когда твои заказчики — это твои коллеги, сидящие в соседнем кабинете, обратная связь доходит почти моментально. Часто можно получить запрос какой-то новой функциональности прямо в коридоре, пока идёшь за чаем.

Ещё коллеги дают очень конструктивные советы, сразу с предложениями, как можно было бы решить ту или иную проблему — это очень ценно, мы стараемся это использовать, и нам это очень помогает.

Второе заметное отличие — метрики. Работая в продуктовой команде, можно отслеживать динамику роста пользователей, сколько из них используют всё больше функций, кто благодаря новому функционалу переходит на более широкие тарифные планы.

В случае с внутренними пользователями подобные вещи отслеживать немного сложнее, ведь динамика уже не будет так сильно зависеть от качества твоего продукта. Да, можно следить, какой функционал зашёл круче, но опять же, как понять, сколько по факту денег это сэкономило или принесло компании? Вот здесь метрики для нас всегда немного сложный вопрос.

Кто-то, возможно, мог бы задуматься, что когда твою работу не видит конечный пользователь — это немного обидно. Но в зоне ответственности нашей команды есть сервисы, от которых зависит напрямую работа множества других продуктов. Поэтому, если у нас что-то пойдёт не так, пользователь это точно заметит. Так что таких сожалений по этому поводу мы точно не испытываем.

А во всём другом, как мне кажется, это всё очень близко к работе продуктового разработчика. Всё так же важно следить за качеством продукта, за тем, чтобы сервис был удобен пользователю, а новый функционал доставлялся вовремя.

— А «разработка для разработчиков» отличается от «разработки для Sales»?

— Есть то очевидное отличие, что зачастую разработчикам не нужен интерфейс, нужно только API. Но, по-моему, главное остаётся общим: и разработчикам, и команде Sales важно, чтобы сервис был стабилен, быстро работал и был удобен в использовании. Поэтому проекты пишутся с одинаковыми требованиями к ним.

— По поводу «автономии команд» в SEMrush для начала спросим вот что: в итоге стек используемых технологий в компании оказывается очень разнообразным? Что используют чаще всего?

— Действительно, поскольку команда сама выбирает, что будет использовать, а команд разработки у нас уже 28, общий стек технологий у нас достаточно большой. Список охватывает практически все популярные фреймворки и библиотеки, попробую перечислить самые часто встречаемые: в новых проектах лидируют React, Redux, Mobx, встречается Vue и Angular, кто-то пишет на TypeScript. Некоторые команды используют Saga. Есть опыт использования Node.js на бэкенде. В сервисах, написанных чуть раньше, часто можно встретить Backbone.
Среди сборщиков тоже свободный выбор: от grunt и gulp до webpack первой и второй версии и babel.

— А конкретно вы с чем работаете?

— В новых проектах наша команда использует React/Redux, webpack, babel. Выбор на такие технологии пал в силу их популярности в нашей компании — можно всегда получить хороший совет от коллег, либо найти его среди множества информации в интернете. В некоторых более старых проектах часто сталкиваемся с Backbone, gulp и grunt.

— Условно говоря, обычно в компании такого размера есть «большой дядя», который думает о скучных вещах вроде обратной совместимости и не даёт командам заиграться с блестящими новыми штучками. А в вашем случае этого дяди нет. К чему это приводит, часто ли приходится «бить себя по рукам», чтобы не заиграться?

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

Почему-то, когда узнают, что у нас нет того самого «большого дяди», многим начинает казаться, что у нас тут полная анархия, каждый творит, что хочет, и вообще это всё похоже на какой-то зоопарк с бешеными зверюшками.

На самом деле нет. Нужно понимать: когда такого управления нет сверху, оно должно появиться в тебе и в каждом из разработчиков. Ты сам себе должен стать «большим дядей».

Потому что когда ты точно знаешь, что это ты ответственен за всё, что у тебя в продукте происходит, у тебя и взгляд на все эти новомодные штучки меняется. Не хочется уже тащить «в дом» всё подряд, хочется только самое лучшее, надёжное и проверенное. И это становится не только приоритетом компании, но и твоим личным приоритетом и даже желанием.

Естественно, новые фреймворки выходят, но кто мешает пощупать их сначала на чём-то маленьком, может быть, даже и не на пользователе находящемся? Обычно всё так и происходит: попробовал на чём-то простом — затянуло, поделился опытом с коллегами, выяснил, что ты такой не один, и вот тут уже можно и подумать: «А не самое ли время это где-то ещё использовать на рабочих задачах».

— К вопросу о «поделился опытом с коллегами»: раз у вас разные команды обладают совершенно разными знаниями, насколько активно внутри компании происходит обмен ими?

— У нас существует практика обмена знаниями, компания старается её развивать и всячески мотивировать разработчиков в ней участвовать.

Пару лет назад у нас просто проходили встречи, когда кто-то говорил: «Хочу всем рассказать про вот такую классную штуку». Заинтересованные собирались, происходил доклад.

Затем, чтобы сделать процесс ещё активнее, мы завели доску, на которой любой человек из компании может повесить стикер с темой выступления, которое он хотел бы рассказать или послушать. Получаются две колонки: «могу рассказать» и «хочу послушать». Спрос и предложение.

Выступления происходят примерно раз в одну-две недели, причём часто они получаются действительно полезными. Кто-то рассказывает, как внедрил новые инструменты. Кто-то о какой-то сложной задаче: почему получилась или нет, какие выводы сделали. Часто происходят доклады между различными гильдиями: QA рассказывают, как писать автотесты, фронтендеры — как обуздать свой первый
.

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

Чтобы нам было повеселее, на такие доклады даже привозят пиццу, и знания усваиваются с двойной скоростью!

Ещё возникают ситуации, когда не знаешь, как лучше решить какую-то задачу, а надо срочно, нет времени ждать доклада. Для таких случаев всегда есть тематический общий чат, где можно кинуть клич и спросить, был ли у кого-то опыт решения подобной проблемы. Очень часто кто-то находится.

А если нет, то это отличный повод потом всем рассказать на отдельном выступлении, чем всё закончилось.

— А возникает ли соперничество между разными командами, сравнивают ли «у кого какие-то метрики лучше окажутся»?

— Все команды очень разные: у каждой свой продукт, свои цели, свой рабочий процесс, поэтому, даже если они и используют схожие метрики, то шкалы у всех очень разные и показатели тоже.

И если попытаться кого-то сравнивать, то это будет сравнение на уровне «вот они — молодцы, а мы что-то не очень». Но это «не очень» будет даже не относительно другой команды, а относительно ваших собственных результатов.

Поэтому мне кажется, что тут скорее соперничество с самим собой. Сделать лучше, чем делал. Не получилось — понять почему, найти проблему, устранить и снова попытаться. А успех коллег — это скорее только мотиватор для твоего собственного.

— В случае с JavaScript часто говорят о разнообразной боли — «зоопарк фреймворков», «что делать с зависимостями» и так далее. Во-первых, насколько вы в SEMrush ощущаете эти болевые места, а во-вторых, влияет ли как-то «автономия команд» на это?

— Конечно, мы тоже ощущаем сложности. И с обновлением зависимостей, когда у тебя огромный продукт написан уже несколько лет назад и нет желающих сейчас его переписывать. И зоопарк фреймворков из прошлых лет и новых продуктов — это всё нам знакомо.

Вот хороший вопрос про автономию команд. С одной стороны, автономия позволяет тебе делать свой продукт, ни от кого не зависящий, обновлять зависимости сразу же, как только это понадобится, использовать фреймворки, которые ты считаешь подходящими для конкретной задачи. Когда ты ни от кого не зависишь — ты сам себе мастер, поэтому многие проблемы решаются быстрее и проще.

Вся боль начинается в элементах, где пересекаются несколько продуктов — в общих частях. Например, хочется вынести какие-то инструменты в межкомандное использование. Но все, кто будут их использовать, сразу теряют автономность. Успеют ли другие команды обновить свой инструмент к моменту, когда ты захочешь обновить свой? И тут всегда получается очень болезненный выбор.

Нужно думать, как будет лучше для пользователей и для дальнейшей разработки. И надо идти и договариваться, тут нет какого-то общего варианта решения вопроса, который всех бы устроил. Здесь и начинается включение того самого «большого дяди». Продумать наперёд, как сделать лучше, чтобы хорошо было уже сейчас.

Вообще, мне кажется, «нормально делай — нормально будет» — это очень правильный девиз. Потому что практически все проблемы, на которые мы натыкаемся — это остатки каких-то не до конца решённых задач, и зачастую наших же. Если почаще думать о пользователе, о той ценности, которую ты ему несёшь, и о том, как это будет поддерживать человек, работающий после тебя, некоторые проблемы начнут решаться сами собой.

По крайней мере, мне бы самой хотелось максимально приблизиться к подобному подходу в работе, и ждать того же от своих коллег.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331692/


Метки:  

Пример построения процесса тестирования OpenStack на Jenkins CI

Понедельник, 26 Июня 2017 г. 16:27 + в цитатник
Процесс тестирования можно построить разными способами. Один из эффективных методов автоматизации процесса тестирования это непрерывное тестирование в рамках непрерывной поставки ПО. Непрерывное тестирование позволяет стабилизировать и улучшить качество кода. Т.к. любое приложение начинается с разработки, то необходимо внедрять полноценное тестирование в циклы разработки.

Основная идея непрерывной поставки в том, чтобы построить конвейер (Deployment Pipeline), позволяющий каждому изменению в системе контроля версий попасть в боевое окружение стандартным и полностью автоматизированным способом.

Пример построения Deployment Pipeline на Jenkins для первой части:
1. Создание pipeline проекта
1.1. “Создать item” — ввести название и выбрать конфигурацию pipeline
1.2. В поле “GitHub project” ввести адрес до репозитория
1.3. Выбрать чекбокс “Опрашивать SCM об изменениях” и настроить расписание на проверку репозитория каждую минуту “* * * * *”
1.4. В поле “Pipeline script” ввести шаги проекта
node{
stage 'Deploy'
build 'Deploy_CHECK'
stage 'Sonar_analysis'
build job: 'Sonar_analysis', parameters: [string(name: 'STAND', value: 'CHECK')]
stage 'Unit tests'
build job: 'Unit_tests', parameters: [string(name: 'STAND', value: 'CHECK')]
stage 'Deploy DEV'
build 'Deploy_DEV'
stage 'Unit tests'
build job: 'Unit_tests', parameters: [string(name: 'STAND', value: 'DEV')]
stage 'Acceptance_test'
build 'Acceptance_test'
stage 'Smoke_tests'
build job: 'Smoke_tests', parameters: [string(name: '', value: 'DEV')]
}

2. Внести изменение в репозиторий
3. В течении минуты pipeline увидит новое изменение в репозитории и запустит проверку

Рис.1. Пример запуска проверок на CHECK и DEV окружениях.
image

Рис.2. Результат выполнения одного из этапов проверок.
image

Рис.3. Обнаружение ошибки на одном из шагов выполнения работы
image

Пример построения Deployment Pipeline на Jenkins для второй части:
1. Создание pipeline проекта
1.1. “Создать item” — ввести название и выбрать конфигурацию pipeline
1.2. В поле “GitHub project” ввести адрес до репозитория
1.3. Выбрать чекбокс “Опрашивать SCM об изменениях” и настроить расписание на проверку репозитория каждую минуту “* * * * *”
1.4. В поле “Pipeline script” ввести шаги проекта
node{
stage 'Deploy QA'
build 'Deploy_QA'
stage 'Compliance tests'
build job: 'chef-compliance', parameters: [string(name: 'STAND', value: 'QA')]
stage 'Functional tests'
build job: 'Tempest', parameters: [string(name: 'STAND', value: 'QA')]
stage 'Performance tests'
build 'Rally'
stage 'Deploy PROD'
build 'Deploy_PROD'
stage 'Smoke tests PROD'
build 'Smoke_tests_PROD'
}


2. Если merge request закрыт успешно, то
2.1. Отрезать ветку develop как release 2.2. Pipeline видит изменения в ветке release*
2.3. Запускается pipeline с проверками

Рис.4. Процесс выполнения pipeline для QA и PRODUCTION окружения
image

Рис.5. Результат успешного выполнения pipeline с развертыванием и запуском тестов на QA и PRODUCTION окружениях
image

Рис.6. Результат не успешного выполнения pipeline с развертыванием и запуском тестов на QA и PRODUCTION окружениях
image

3. Если merge request закрыт с неуспехом, то
3.1. Разработчику, которого изменения были затронуты, отправляется уведомление с указанием ошибки
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331690/


Метки:  

Pure Storage: час //m настал

Понедельник, 26 Июня 2017 г. 16:07 + в цитатник
Привет, Хабр! Многие из вас, кто следит за рынком All-Flash систем, слышали о Pure Storage. О них были упоминания в комментариях, вендорских сравнениях, рейтингах. Одна беда – в России Pure не продавался.

И вот теперь мы готовы рассказать вам следующее: Pure Storage таки добрался до нас и вот-вот выйдет на российский рынок!



Небольшое введение


Я занимаюсь развитием направлений СХД и СРК в «Джете». Одна из сфер моей деятельности – поиск и адаптация новых векторов развития ИТ-инфраструктуры. Среди таких векторов – активный переход на Flash-технологии и связанные с этим изменения в хранении и обработке данных.
Сейчас всем понятно, что использование механических дисков для высокопроизводительных вычислений уходит в прошлое. Грядет эра Flash. Но, как мы видим, сегодняшний All-Flash – это чаще всего просто набитые SSD традиционные дисковые массивы. Тем интереснее для нас совершенно новые игроки, стартапы этого бизнеса – компании, которые не несут за собой груз технологий 20-летней давности.

Сейчас у Pure Storage 3 линейки СХД:
  • FlashArray //m – линейка AF-массивов общего назначения, подходящая как для требовательных приложений, так и для обычных систем (пока только //m-линейка будет доступна в России);
  • FlashArray //x – NVMe Flash-массив для самых критичных к производительности приложений;
  • FlashBlade – горизонтально масштабируемый файловый и объектный AF-массив.

В чем фишка Pure?


На Pure Storage мы обратили внимание более 5 лет назад. Тогда ничего, кроме ироничной улыбки, эти ребята не вызывали. Железяки от Supermicro с линуксом на борту, какие-то шаманские пляски с кодом и сторонними приложениями, красивая идея и не самая лучшая реализация. Но время идет, все меняется, и Pure Storage смог создать действительно интересный продукт, который начал захватывать Америку и Европу.

Почему Pure так интересен нам? Во-первых, это архитектура, изначально созданная под Flash. Ребята много лет делали продукт, который учитывал все нюансы именно Flash, а не механических дисков. Например, компрессия и дедупликация работают всегда - их нельзя выключить, они зашиты в сам код, в механизмы работы с данными.

Во-вторых, он работает просто и быстро. Помните RAID-группы, LDEV'ы, пулы, tiering, партиционирование, spare-диски? Вот этого ничего нет. Запустили массив – получили емкость и производительность. Добавили дисков – получили больше емкости и производительности. Ни о чем не надо думать, все просто р-а-б-о-т-а-е-т.

Кроме того, интересна сама архитектура. С одной стороны, это классический двухконтроллерный массив, с другой – есть особенность: данные не кэшируются на запись в оперативной памяти, все хранится на внешних NVMe-модулях. Таким образом, можно на ходу вынимать контроллеры, менять их на другие, апгрейдиться внутри линейки, между линейками – все это без даунтаймов. И без потерь производительности - система не дает себя нагружать более чем на 50% в штатном режиме.

А самое вкусное - это отличия бизнес-модели Pure в части продажи и поддержки.

EverGreen


Программа «вечно актуальной СХД», направленная на постоянное обновление массива:
  • каждые 3 года в рамках сервисного контракта Pure Storage бесплатно меняет клиентам контроллеры на актуальные модели (пока вы на поддержке, у вас всегда новые контроллеры);
  • новые поколения массивов не требуют замены дисков, никаких миграций;
  • пожизненная гарантия на накопители (пока есть поддержка);
  • Pure меняет устаревшие диски на новые бесплатно, если заказчик увеличивает емкость в 4 раза;
  • все лицензии включены (даже если будет выходить новый функционал).

Таким образом, ребята говорят: «Купите у нас массив один раз, и вам больше никогда не придется его перепокупать». Зарабатывать они, очевидно, собираются на поддержке и на расширениях емкости. Подход интересный, ничего не скажешь.

RIGHT-SIZE GUARANTEE. Гарантия ёмкости


Программа гарантии экономии емкости с помощью технологий дедупликации и сжатия. На этапе просчета конфигурации вы сообщаете, какие данные будете хранить на массиве (например, VMware 30ТБ, Oracle 15ТБ). Затем интегратор с вендором готовят предложение, в котором будет прописан коэффициент эффективности для представленных данных (например, 5:1). В этом случае Pure Storage юридически гарантирует этот коэффициент перед продажей. Если он не будет соответствовать реальному – привезут дополнительные диски.

На картинке ниже представлен средний коэффициент по всей базе Pure Storage (несколько тысяч массивов), который составляет чуть более 5:1



Счетчик эффективности публикуется на сайте производителя и обновляется круглосуточно

www.purestorage.com/why-pure/efficient.html

Вроде многие производители предлагают нечто подобное, но часто это просто маркетинг. Pure же возводят эффективность хранения в культ: дает юридические гарантии, прописывает это в предложениях. Посмотрим, как оно будет в реальности.

Pure1


Вместе с покупкой массива вы бесплатно получаете доступ к облачному сервису мониторинга Pure1. Он включает не только проактивный сервис, когда кейсы открываются производителем автоматически в момент возникновения инцидента, но и предиктивную аналитику из облака. Многочисленные диагностические метрики поступают с интервалом в 30 секунд со всей инсталлированной базы в систему машинного обучения Pure1 Meta, которая позволяет предотвращать проблемы еще до того, как они реально наступили. По словам производителя, эта технология помогла предотвратить более 170 кейсов 1 приоритета еще до того, как они были открыты.


Вышеперечисленные особенности, собранные в одном продукте, заставляют совсем иначе смотреть на этот массив. Pure способен нарушить установившийся статус-кво на рынке СХД, перевернуть отношение к дисковым массивам, облегчить переход на Flash-технологии.

И где Pure в России?


Проблема одна – шифрование. Долгое время Pure Storage не мог выйти на российский рынок из-за ограничений на ввоз систем с шифрованием. На данный момент выпущена специальная версия ОС для России и Китая, на которую уже получена нотификация ФСБ. Теперь Pure готов выйти на наш рынок – мы ожидаем первых поставок уже в августе.

«Джет» является высшим сервисным партнером Pure Storage. Сейчас мы активно занимаемся получением собственного демо-стенда – целимся на июль (очень может быть, что у нас стенд будет даже раньше, чем у самого вендора в России). После этого будем готовы предоставить вам настоящий Geek Porn – полный отчет о тестировании массива самого быстроразвивающегося Flash-стартапа. Как говорится, не переключайтесь! ;)

После наших тестов вы также сможете испытать новинку в деле — демо-стенд будет доступен для всех наших заказчиков. Следите за новостями.

Рад буду ответить на ваши вопросы в комментариях. Если задача требует более детальной проработки, пишите на адрес belyaevskiy@jet.msk.su.

Автор статьи: Владимир Беляевский, руководитель направления СХД компании «Инфосистемы Джет»

Ссылки


Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/331682/


Командная разработка системы на базе MS Dynamics CRM

Понедельник, 26 Июня 2017 г. 14:41 + в цитатник

Метки:  

Поиск сообщений в rss_rss_hh_new
Страницы: 1437 ... 1024 1023 [1022] 1021 1020 ..
.. 1 Календарь